#NAME
#  abcCode.pm - Calculate Gore/Breathnach codes from ABC notation

#SYNOPSIS
#  use abcCode;
#  $OBJ = new abcCode;
#  ($GBcode,$JCcode,$UDcode,$Used) = $OBJ->abcCode($K,$L,$M,$music,...);

#DESCRIPTION
#  Given a chunk of ABC notation and a (presumed)  tonic  note,  calculate  a
#  code that can be used to characterize the tune.

#  The music can be passed in as one string or as an array of strings. In the
#  latter case, we will treat each string as the start of a new "chunk".

#  For each string of music, we first discard any  pickup  notes,  i.e.,  any
#  notes  before  the first bar line.  We then use the $M and $L values (from
#  the M:  and L:  header lines) to divide each group of notes up into beats.
#  We then take the first note of each beat, and calculate the scale distance
#  from the tonic, giving a number in the range 1-7, and 0 for rests.   These
#  distances are catenated into a string, which is the code that we return.

#  We  ignore  all modes, ornaments and accidentals.  This means that we will
#  produce the same code for the major and minor versions of  a  tune.   (The
#  Gore code does this; the Breathnach code includes accidentals.)

#PARAMETERS
#  The expected parameters to the abcCode() function are:

#  $K  is the tune's key, from which we extract the first char as the tonic.
#  $L  is the tune's basic note length.
#  $M  is the tune's meter, not used if $L is defined.
#  $music
#    The rest of the args should be strings of ABC music notation.  We use as
#    much of it as we need and ignore the rest.

#RETURNS
#  The return value is a list of three strings:

#  $GBcode
#    is the Gore-Breathnach code for the music.  It is a list  of  the  scale
#    steps, using the first letter in $K as the tonic, or 1, and ignoring any
#    notes that are not at a multiple of $L.
#  $JCcode
#    is JC's first-difference code, calculated by subtracing adjacent  values
#    in $GBcode, and ignoring the repeated numbers due to long notes.
#  $Used
#    is  the portion of the music that was actually used to calculate the two
#    codes. It usually matches the $JCcode, since that uses more notes.  This
#    is mostly useful for debugging purposes.

#BUGS
#  This coding scheme is useful but not perfect.  There are a lot of bad  ABC
#  coding practices that can produce poor or useless codes.

#SEE ALSO
#  There is a program abc2code that reads files, feeds them to abcCode(), and
#  displays the results.  It should be in the same directory as this module.

#AUTHOR
#  John Chambers <jc@trillian.mit.edu>

package abcCode;

my($GBcode) = '';		# Gore-Breathnach code string.
my($GBmax)  = 16;		# Max length of GBcode.
my($JCcode) = '';		# JC code is "derivative" of GBcode string.
my($JCmax)  = 15;		# Max length of JCcode.
my($UDcode) = '';		# UD code is list of up/down changes.
my($UDmax)  = 15;		# Max length of UDcode.
my($Length) = 0;		# Length currently accumulated.
my($Cnote)  = '';		# Current note's pitch (letter).
my($Cstep)  = 0;		# Current note's step (absolute).
my($Pnote)  = '';		# Previous note's pitch (letter).
my($Pstep)  = 0;		# Previous note's step (absolute).
my($Pval)   = undef;	# Previous note's pitch value (steps from C).
my($Used)   = '';		# Portion of music used for codes.
my($V)      = 1;		# Verbose level.

$" = '","';
$abcnote = '[A-Ga-g][,\']*[\d/]';	# Pattern to match an ABC note

sub new { my($O) = $_[0]; return $O}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
#    &abcCode($O,$K,$L,$M,...)
# One or more lines of ABC music (i.e., the notes, not the header  lines)  is
# passed  to us in addition to the four args.  We parse the music just enough
# to extract the notes and their lengths.  We pass the notes to abcNote()  to
# do the actual code calculation.
#
# The return value is a list of three strings:
#    ($GBcode,$JCcode,$UDcode,$Used);
# where the strings are:
#   $GBcode is the Gore-Breathnach code.
#   $JCcode is JC's first-difference code.
#   $UDcode is the up/down code.
#   $Used   is the ABC used to generate the codes.
# Note that the $Used string excludes several things:
#   1. Anything before the first bar line is ignored.
#   2. Only the first note of chords is used.
#   3. Grace notes are ignored.
#   4. Accompaniment chords are ignored.
#   5. > and < are ignored (but numeric lengths are honored.

sub abcCode {
	my($F) = "abcCode"; print "$F(\"@_\")\n" if $V>4;
	my($O) = shift;			# Object pointer
	my($K) = shift || '';	# Key, i.e., the tonic with some extra junk.
	my($L) = shift || '';	# Length, from L: line.
	my($M) = shift || '';	# Meter,  from L: line.
	if    ($M eq 'C'   ) {$M = '4/4'}	# Common time.
	elsif ($M eq 'C|'  ) {$M = '2/2'}	# Cut time.
	elsif ($M eq 'none') {$M = '4/4'}	# Kludge for free meter.
	elsif ($M =~ m"^([-+\d.])/(\d+)$") {}
	else  {$M = '1/4'}
	my($U) = $M || $L || '1/4';	# Unit length
	print "$F: K=\"$K\" L=\"$L\" M=\"$M U=\"$U\"\"\n" if $V>4;
	local $bars = 0;
	local($c,$x);
	$GBcode = $JCcode = $UDcode = $Pnote = $Cnote = $Used = '';
	$Length = $Cstep = $Pstep = 0;
	$Pval = undef;
	$K =~ s/(.).*/$1/;
	$M = '1/4' unless $M;
	$L = $M unless $L;
	$L =~ s/^\d+/1/;
	$U =~ s/^\d+/1/;
	print STDERR "$F: K=\"$K\" U=\"$U\" M=\"$M\"\n" if $V>4;
	for $x (@_) {								# One or more strings.
		print "$F: Line \"$x\"\n" if $V>5;
		while ($x) {
			last if (length($GBcode) >= $GBmax)
			     && (length($JCcode) >= $JCmax)
				 && (length($UDcode) >= $UDmax);
			print "$F: \"$x\"\n" if $V>5;
			if ($x =~ s/^[\r\s]+//) {
				print "$F: Skip space.\n" if $V>5;
#				$Used .= ' ' if $bars;
			} elsif ($x =~ s/^([\|:]+)//) {		# Bar lines.
				$Used .= '|';
				++$bars;
				print "$F: \"$1\" bar line.\n" if $V>3;
				$Length = 0;					# Discard any fractional beat.
			} elsif ($x =~ s/^("[^"]*")//) {	# Discard gchords.
				print "$F: Drop \"$1\" (gchord).\n" if $V>5;
			} elsif ($x =~ s"^\[([A-Ga-g])([,']*)([\d/]*)[^\]]\]"") {	# Chord
				next unless $bars > 0;
				print "$F: \"$x\"\n" if $V>6;
				$pitch  = $1;
				$octave = $2;
				$length = $3;
				$Used .= "$1$2$3";
				print "$F: pitch=\"$pitch\" octave=\"$octave\" length=\"$length\".\n" if $V>3;
				$c .= $O->abcNote($K,$U,$L,$pitch,$octave,$length);
			} elsif ($x =~ s"^([A-Ga-g])([,']*)([\d/]*)([<>]*)"") {	# Note
				next unless $bars > 0;
				print "$F: \"$x\"\n" if $V>6;
				$pitch  = $1;
				$octave = $2;
				$length = $3;
				$mult   = $4;
				$Used .= "$1$2$3";
				print "$F: pitch=\"$pitch\" octave=\"$octave\" length=\"$length\".\n" if $V>3;
				$c .= $O->abcNote($K,$U,$L,$pitch,$octave,$length);
			} elsif ($x =~ s/^(\{[^}]*\})//) {	# Grace notes..
				print "$F: Comment \"$x\" ignored.\n" if $V>5;
			} elsif ($x =~ s/^\%\s*//) {		# Comments.
				print "$F: Comment \"$x\" ignored.\n" if $V>5;
				next;
			} else {
				$x =~ s"(.)"";
				print "$F: Char \"$1\" ignored.\n" if $V>5;
#				$Used .= $1 if $bars;
			}
		}
	}
	print "$F: Stop with UDcode=\"$UDcode\" JCcode=\"$JCcode\" GBcode=\"$GBcode\"\n" if $V>4;
	$GBcode = substr(($GBcode . ('_' x $GBmax)),0,$GBmax) if length($GBcode) > $GBmax;
	$JCcode = substr(($JCcode . ('_' x $JCmax)),0,$JCmax) if length($JCcode) > $JCmax;
	$UDcode = substr(($UDcode . ('_' x $UDmax)),0,$UDmax) if length($UDcode) > $UDmax;
	return ($GBcode,$JCcode,$UDcode,$Used);
}

my %abcvals = (''=>0,
	'C'=>1,'D'=>2,'E'=>3,'F'=>4,'G'=>5,'A'=>6,'B'=>7,'H'=>6,
	'c'=>8,'d'=>9,'e'=>10,'f'=>11,'g'=>12,'a'=>13,'b'=>14);
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
#   abcNote($O,$K,$U,$L,$N,$o,$D)
# This routine is called for each note encountered.  We add the  duration  to
# the  running total, and then clip off as many notes of length $U as we can,
# adding each to the growing $GBcode string.

sub abcNote {
	my($F) = "abcNote"; # $V = $main::V; print "$F(\"@_\")\n" if $V>4;
	my(	$O,			# Object.
		$Key,		# Key (tonic + mode)
		$Unit,		# Unit length for code calculations.
		$Len,		# Note length (from Len: line).
		$Note,		# Note (letter).
		$Octave,	# Octave (commas or apostrophes).
		$Dur)		# Duration (relative to $Len).
			= @_;
	my($Kstep);		# Note's step within current key.
	my($diff);		# Change of pitch from previous note.
	my($len);		# Note's length in Len: units.
	my($pval);		# Note's pitch relative to C.
	my($ties) = 0;	# Counter for long/tied notes.
	my($Tonic);		# Tonic note.
	my($left);
	if ($Key =~ /([A-Ga-g])/) {
		$Tonic = uc($1);	# Tonic should be upper case
	} elsif ($Key =~ /([Hh])/) {
		$Tonic = 'A';		# K:H and K:Hp use A for the nominal tonic
	} else {
		$Tonic = 'C';		# It might also be Am or Ddor or ...
	}
	print "$F: ----- Note=\"$Note\" Dur=\"$Dur\" Tonic=\"$Tonic\" Key=\"$Key\" Len=\"$Len\" Unit=\"$Unit\" \n" if $V>4;
	print "$F: Pnote=\"$Pnote\" Pstep=$Pstep Pval=$Pval Cnote=\"$Cnote\" Cstep=$Cstep Length=$Length.\n" if $V>4;
	$Dur =~ s"^/"1/";	# Canonicalize the duration.
	$Dur =~ s"/$"/2";
	while ($Dur =~ s"//(\d+)"'/'.($1*2)"e) {
		print "$F: Dur=\"$Dur\" \n" if $V>4;
	}
	$Dur = 1 unless $Dur;
	$len = $O->frmul($Dur,$Len);				# Convert to absolute length.
	print "$F: dur=\"$Dur\" len=\"$len\" Tonic=\"$Tonic\"\n" if $V>5;
	$Cstep = $pval = $abcvals{$Cnote = $Note};	# Current scale step.
	while ($Octave =~ s/,//) {$pval -= 7}
	while ($Octave =~ s/'//) {$pval += 7}
	print "$F: Cnote=\"$Cnote\" Cstep=\"$Cstep Pnote=\"$Pnote\" Pstep=\"$Pstep\" pval=$pval.\n" if $V>5;
	if (defined $Pval) {
		$diff = $pval - $Pval;
		$udcode = ($diff < 0) ? 'd' : ($diff > 0) ? 'u' : '';
		$UDcode .= $udcode;
		print "$F: pval=$pval Pval=$Pval diff=$diff udcode='$udcode' UDcode=\"$UDcode\"\n" if $V>2;
	}
	$Pval = $pval;
	if (eval($Length) == 0) {
		print "$F: First note in beat.\n" if $V>5;
		$O->putNote($Cnote,$Cstep,$Tonic);
	}
	$Length = $O->fradd($Length,$len);
	print "$F: Length=\"$Length\" accumulated since last beat.\n" if $V>5;
	while (($left = eval($Length)) >= eval($Unit)) {
		print "$F: Length=\"$Length\" contains a $Unit beat.\n" if $V>5;
		$Length = $O->fradd($Length,"-$Unit");
		print "$F: Length adjusted to\"$Length\".\n" if $V>5;
		if (eval($Length) > 0) {
			print "$F: Length $Length starts new beat.\n" if $V>5;
			$O->putNote($Cnote,$Cstep,$Tonic);
		}
	}
	print "$F: $Length left.\n" if $V>5;
	$Cstep;
}

sub putNote { my($F) = "putNote"; print "$F(" . join(',',@_) . ")\n" if $V>4;
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Add one note to the code string(s).
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	my($O) = shift;
	my($cnote,$cstep,$tonic) = @_;
	my($diff);		# Offset from previous note to current note.
	$tonic = 'C' unless $tonic;
	print "$F: cnote=\"$cnote\" cstep=\"$cstep\" tonic=$tonic.\n" if $V>4;
	unless (defined $abcvals{$tonic}) {
		print "$F: ### No abcval for '$tonic'\n" if $V>0;
		$tonic = 'C';
	}
	$kstep = $cstep - $abcvals{$tonic} + 1;	# Current step within key.
	$kstep += 7 while $kstep < 1;	# Adjust to [1,7]
	$kstep -= 7 while $kstep > 7;
	if ($Pnote) {
		print "$F: Calculate change from previous note Pnote=\"$Pnote\" Pstep=$Pstep.\n" if $V>5;
		print "$F: diff=$diff JCcode=\"$JCcode\" GBcode=\"$GBcode\"\n" if $V>4;
		$diff = $cstep - $Pstep;
		$diff += 8 while $diff < 0;	# Adjust to [0,7]
		$diff -= 8 while $diff > 7;
		$JCcode .= $diff;
		print "$F: diff=$diff JCcode=\"$JCcode\" GBcode=\"$GBcode\"\n" if $V>4;
	}
	$GBcode .= $kstep;		# Gore-Breathnach code.

	$Pnote = $cnote;
	$Pstep = $cstep;
	print "$F: Pnote=\"$Pnote\" Pstep=\"$Pstep\" Length=$Length JCcode=\"$JCcode\" GBcode=\"$GBcode\"\n" if $V>4;
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Fraction adder.  We accept any number of args  and  return  the  sum  as  a
# fraction.   The  args may be integers or fractions.  This currently doesn't
# give sensible results for negative denominators.  If there are no args,  we
# return "0/1".  We don't remove common factors in the result.

sub fradd {
	my($F) = "fradd"; print "$F(\"@_\")\n" if $V>6;
	my($O) = shift;
	my($N) = '0';
	my($D) = '1';
	my($a,$m,$M,$n,$d);
loop:
	for $a (@_) {
		print "$F: N=\"$N\" D=\"$D\" a=\"$a\".\n" if $V>7;
		if (($n,$d) = ($a =~ m"^(-*\d+)/(\d+)$")) {
		} elsif ($a =~ m"^(-*\d+)") {
			$n = $a + 0; $d = 1;
		} else {
			print STDERR "$F: \"$a\" not a number.\n" if $V>0;
		#	$n = 0; $d = 1;
			next loop;
		}
		print "$F: N=\"$N\" D=\"$D\" n=\"$n\" d=\"$d\"\n" if $V>7;
		if ($d != $D) {
			my($gcd) = $O->GCD($d,$D) || 1;
			print "$F: N=\"$N\" D=\"$D\" n=\"$n\" d=\"$d\" gcd=\"$gcd\"\n" if $V>7;
			$m = $d / $gcd; $M = $D / $gcd;
			$N *= $m; $D *= $m;
			$n *= $M; $d *= $M;
			print "$F: N=\"$N\" D=\"$D\" n=\"$n\" d=\"$d\" M=$M m=$m.\n" if $V>7;
		}
		$N += $n;
		print "$F: N=\"$N\" D=\"$D\".\n" if $V>7;
	}
	return "$N/$D";
}

sub frmul {
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Fraction multiplier. We accept any number of args and return the product as #
# a  fraction.  Numerators and denominators can be negative.  If there are no #
# args, we return "1/1".  We don't remove common factors in the result.       #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	my($F) = "frmul"; print "$F(\"@_\")\n" if $V>6;
	my($O) = shift;
	my($N) = '1';
	my($D) = '1';
	my($n,$d);
loop:
	for $a (@_) {
		print "$F: N=\"$N\" D=\"$D\" a=\"$a\".\n" if $V>7;
		if ($a =~ m"^(-*\d+)/(-*\d+)$") {
			$n = int($1);	# Numerator
			$d = int($2);	# Denominator
		} elsif ($a =~ m"^(-*\d*)") {
			$n = int($1);
			$d = 1;
		} else {
			print STDERR "$F: \"$a\" not a number.\n" if $V>0;
		#	$n = 1; $d = 1;
			next loop;
		}
		print "$F: N=\"$N\" D=\"$D\" n=\"$n\" d=\"$d\"\n" if $V>7;
		$N *= $n;
		$D *= $d;
		print "$F: N=\"$N\" D=\"$D\".\n" if $V>7;
	}
	return "$N/$D";
}

sub GCD {my $F = "GCD";
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
# Euclid's algorithm to calculate GCD(X,Y).  It's faster with X>Y, but either #
# order will work.  X and Y should be integers. Note that we always return an #
# integer > 0, so it's safe to use the result as a divisor.                   #
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	print "$F(\"" . join(',',@_) . "\")\n" if $V>7;
	my($O,$X,$Y) = @_;
	return 1 unless ($X = int($X)) && ($Y = int($Y));
	return $X if ($X == $Y);
	return $O->GCD($Y,$X) if $X < $Y;
	my($R) = $X % $Y;
	return $Y unless $R;
	return $O->GCD($Y,$R);
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - #

sub foo {
	my($F) = "foo"; # $V = $main::V; print "$F(\"@_\")\n" if $V>4;
	my($O) = shift;
}

1;
