#!perl
#
# Command line interface to the atonal routines in the Music::AtonalUtil
# module. Run perldoc(1) on this script for additional documentation.
#
# ZSH completion script available in the zsh-compdef directory of the
# App::MusicTools distribution.

# XXX add --tension to show Cope tension values (off by default
# otherwise)

# XXX improve emit_pitch_set (returns a string, so caller can then do
# what it will -- or have LilyPondUtil sub that knows how to format a
# pitch set? (among other possible code cleanups/simplifications)

use strict;
use warnings;

use Getopt::Long qw/GetOptionsFromArray/;
use List::MoreUtils qw/all any/;
use Music::AtonalUtil    ();
use Music::LilyPondUtil  ();
use Music::Tension::Cope ();
use Parse::Range qw/parse_range/;
use Scalar::Util qw/looks_like_number/;

my %modes = (
  basic                  => \&basic,
  circular_permute       => \&circular_permute,
  complement             => \&complement,
  equivs                 => \&equivs,
  findall                => \&findall,
  findin                 => \&findin,
  forte2pcs              => \&forte2pcs,
  fnums                  => \&fnums,
  interval_class_content => \&interval_class_content,
  invariance_matrix      => \&invariance_matrix,
  invariants             => \&invariants,
  invert                 => \&invert,
  multiply               => \&multiply,
  normal_form            => \&normal_form,
  pcs2forte              => \&pcs2forte,
  pitch2intervalclass    => \&pitch2intervalclass,
  prime_form             => \&prime_form,
  recipe                 => \&recipe,
  retrograde             => \&retrograde,
  rotate                 => \&rotate,
  set_complex            => \&set_complex,
  subsets                => \&subsets,
  tcs                    => \&tcs,
  tcis                   => \&tcis,
  tension                => \&tension,
  transpose              => \&transpose,
  transpose_invert       => \&transpose_invert,
  variances              => \&variances,
  zrelation              => \&zrelation,
);

my ( $Flag_Flat, $Flag_Lyout );
my @Std_Opts = (
  'flats' => \$Flag_Flat,
  'ly'    => \$Flag_Lyout,
);

my @leading_args;
while ( @ARGV and !exists $modes{ $ARGV[0] } ) {
  push @leading_args, shift @ARGV;
}

GetOptionsFromArray(
  \@leading_args,
  @Std_Opts,
  'help'           => \&print_help,
  'listmodes'      => sub { print "$_\n" for sort keys %modes; exit 0 },
  'scaledegrees=s' => \my $scale_degrees,
) or print_help();
$scale_degrees //= Music::AtonalUtil->new->scale_degrees();
my $mode = shift;

print_help() if !defined $mode || !exists $modes{$mode};

my $atu = Music::AtonalUtil->new( DEG_IN_SCALE => $scale_degrees );
my $lyu = Music::LilyPondUtil->new(
  chrome => ( $Flag_Flat ? 'flats' : 'sharps' ),
  keep_state => 0,
  mode       => 'relative'
);
my $tension = Music::Tension::Cope->new();

$modes{$mode}->(@ARGV);
exit 0;

########################################################################
#
# SUBROUTINES

sub args2pitchset {
  my (@args) = @_;
  my $dis = $atu->scale_degrees;

  if ( !@args or ( @args == 1 and $args[0] eq '-' ) ) {
    chomp( @args = readline STDIN );
  }

  my $pitch_set;
  if ( $args[0] =~ m/\d-/ ) {
    $pitch_set = $atu->forte2pcs( $args[0] );
    die "unknown Forte Number '$args[0]'\n" if !defined $pitch_set;
  } else {
    for my $arg (@args) {
      for my $p ( $arg =~ /([\d\w]+)/g ) {
        if ( $p =~ m/^\d+/ ) {
          push @$pitch_set, $p % $dis;
        } else {
          push @$pitch_set, $lyu->notes2pitches($p);
        }
      }
    }
  }

  return $pitch_set;
}

sub basic {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) || print_help();
  $lyu->chrome('flats') if $Flag_Flat;

  my $pset = args2pitchset(@args);

  emit_pitch_set( $atu->prime_form($pset), rs => ',' );
  emit_pitch_set(
    scalar $atu->interval_class_content($pset),
    lyflag => 0,
    rs     => '',
  );

  my $forte = $atu->pcs2forte($pset) // '';
  my ( $t_avg, $t_min, $t_max, $t_ref ) = $tension->vertical($pset);
  printf "%.03f  %.03f  %.03f\t%s\n", $t_avg, $t_min, $t_max, $forte;
}

sub circular_permute {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) || print_help();
  $lyu->chrome('flats') if $Flag_Flat;
  emit_pitch_set( $atu->circular_permute( args2pitchset(@args) ) );
}

sub complement {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) || print_help();
  $lyu->chrome('flats') if $Flag_Flat;
  emit_pitch_set( $atu->complement( args2pitchset(@args) ) );
}

sub equivs {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) || print_help();
  $lyu->chrome('flats') if $Flag_Flat;

  my ( @transpose, @transpose_invert, %seen );
  my $pset = args2pitchset(@args);

  for my $i ( 0 .. $atu->scale_degrees - 1 ) {
    my $set = $atu->normal_form( $atu->transpose( $pset, $i ) );
    if ( !$seen{"@$set"}++ ) {
      push @transpose, $set;
      my $iset = $atu->normal_form( $atu->transpose_invert( $pset, $i ) );
      push @transpose_invert, $iset if !$seen{"@$iset"}++;
    }
  }

  emit_pitch_set( \@transpose );
  emit_pitch_set( \@transpose_invert );
}

sub findall {
  my (@args) = @_;
  GetOptionsFromArray(
    \@args, @Std_Opts,
    'exclude=s' => \my $excludes,
    'fn=s'      => \my $desired_forte_nums,
    'root=s'    => \my $root_pitch
  );
  $lyu->chrome('flats') if $Flag_Flat;

  my $desired = args2pitchset(@args);
  my %excludes;
  @excludes{ $lyu->notes2pitches( split /[, ]+/, $excludes ) } = ()
    if defined $excludes;
  $root_pitch = $lyu->notes2pitches($root_pitch) if defined $root_pitch;
  my $fn_re = '^[' . join( '', parse_range($desired_forte_nums) ) . ']$';

  my $fnums = $atu->fnums;
  for my $fnum ( sort keys %$fnums ) {
    if ( defined $desired_forte_nums ) {
      ( my $prefix = $fnum ) =~ s/[-].+//;
      next if $prefix !~ m/$fn_re/;
    }
    _findps( $fnums->{$fnum}, $desired, $root_pitch, \%excludes, $fnum );
  }
}

sub findin {
  my (@args) = @_;
  GetOptionsFromArray(
    \@args, @Std_Opts,
    'exclude=s'    => \my $excludes,
    'pitchset|P=s' => \my $base_input,
    'root=s'       => \my $root_pitch
  ) || print_help();
  $lyu->chrome('flats') if $Flag_Flat;

  my $ps_base;
  if ( $base_input =~ m/^\d-/ ) {
    $ps_base = $atu->forte2pcs($base_input);
    die "unknown Forte Number '$base_input'\n" if !defined $ps_base;
  } else {
    $ps_base = args2pitchset( split /[ ,]/, $base_input );
  }
  my $desired = args2pitchset(@args);
  my %excludes;
  @excludes{ $lyu->notes2pitches( split /[, ]+/, $excludes ) } = ()
    if defined $excludes;
  $root_pitch = $lyu->notes2pitches($root_pitch) if defined $root_pitch;

  if ( @$desired > @$ps_base ) {
    die "cannot desire more than is present\n";
  }

  _findps( $ps_base, $desired, $root_pitch, \%excludes );
}

sub _findps {
  my ( $ps_base, $desired, $root_pitch, $excludes, $fnum ) = @_;
  $fnum //= '-';
  $excludes //= {};

  my $ps_width = 24 - ( $Flag_Lyout ? 0 : 6 );

TRANS: for my $i ( 0 .. $atu->scale_degrees - 1 ) {
    my %tps;
    @tps{ @{ $atu->transpose( $ps_base, $i ) } } = ();
    if ( all { exists $tps{$_} } @$desired ) {
      my @pitches = @{ $atu->transpose( $ps_base, $i ) };
      next if defined $root_pitch and $pitches[0] != $root_pitch;
      if (%$excludes) {
        for my $p (@pitches) {
          next TRANS if exists $excludes->{$p};
        }
      }

      my ( $t_avg, $t_min, $t_max, $t_ref ) = $tension->vertical( \@pitches );

      @pitches = $lyu->p2ly(@pitches) if $Flag_Lyout;

      printf "%s\tT(%d)\t%-${ps_width}s\t%.03f  %.03f  %.03f\n", $fnum, $i,
        join( ',', @pitches ), $t_avg, $t_min, $t_max;
    }
  }

TRANSINV: for my $i ( 0 .. $atu->scale_degrees - 1 ) {
    my %ips;
    @ips{ @{ $atu->transpose_invert( $ps_base, $i ) } } = ();
    if ( all { exists $ips{$_} } @$desired ) {
      my @pitches = @{ $atu->transpose_invert( $ps_base, $i ) };
      next if defined $root_pitch and $pitches[0] != $root_pitch;
      if (%$excludes) {
        for my $p (@pitches) {
          next TRANSINV if exists $excludes->{$p};
        }
      }

      my ( $t_avg, $t_min, $t_max, $t_ref ) = $tension->vertical( \@pitches );

      @pitches = $lyu->p2ly(@pitches) if $Flag_Lyout;

      printf "%s\tTi(%d)\t%-${ps_width}s\t%.03f  %.03f  %.03f\n", $fnum, $i,
        join( ',', @pitches ), $t_avg, $t_min, $t_max;
    }
  }
}

sub forte2pcs {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) || print_help();

  emit_pitch_set( $atu->forte2pcs( $args[0] ) );
}

sub fnums {
  my (@args) = @_;

  my $fns = $atu->fnums;
  for my $fn ( sort keys %$fns ) {
    my $pset = $fns->{$fn};
    my $icc  = $atu->interval_class_content($pset);
    my ( $t_avg, $t_min, $t_max, $t_ref ) = $tension->vertical($pset);
    printf "%s\t%-16s\t%-8s\t%.03f  %.03f %.03f\n", $fn, join( ',', @$pset ),
      join( '', @$icc ),
      $t_avg,
      $t_min, $t_max;
  }
}

sub emit_pitch_set {
  my ( $pset, %params ) = @_;

  my $lyify = exists $params{lyflag} ? $params{lyflag} : $Flag_Lyout;
  my $rs    = exists $params{rs}     ? $params{rs}     : ' ';

  my $has_nl = 0;
  my $str    = '';
  for my $i (@$pset) {
    if ( ref $i eq 'ARRAY' ) {
      $has_nl = emit_pitch_set( $i, %params );
    } else {
      $str .= ( $lyify ? $lyu->p2ly($i) : $i ) . $rs;
    }
  }
  $str =~ s/$rs\z//;
  $str .= "\n" unless $has_nl;
  print $str;
  return 1;
}

sub interval_class_content {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts );
  emit_pitch_set(
    scalar $atu->interval_class_content( args2pitchset(@args) ),
    lyflag => 0,
    rs     => '',
  );
}

sub invariance_matrix {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts );
  emit_pitch_set( $atu->invariance_matrix( args2pitchset(@args) ),
    lyflag => 0 );
}

sub invariants {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) || print_help();
  $lyu->chrome('flats') if $Flag_Flat;
  my $ps = args2pitchset(@args);

  my %seen;
  @seen{@$ps} = ();

  my $icc = scalar $atu->interval_class_content($ps);
  print '[',
    join( ' ', map { $Flag_Lyout ? $lyu->p2ly($_) : $_ } @$ps ),
    '] icc ', join( '', @$icc ), "\n";

  my $ps_len = @$ps * ( $Flag_Lyout ? 4 : 2 );

  for my $t ( 1 .. $atu->scale_degrees - 1 ) {
    my $tps = $atu->transpose( $ps, $t );
    my @t_invary;
    for my $p (@$tps) {
      push @t_invary, $p if exists $seen{$p};
    }
    if (@t_invary) {
      printf "%-5s [ %-${ps_len}s ] %s [ %-${ps_len}s ]\n", "T  $t",
        join( ' ', map { $Flag_Lyout ? $lyu->p2ly($_) : $_ } @$tps ),
        'invars',
        join( ' ', map { $Flag_Lyout ? $lyu->p2ly($_) : $_ } @t_invary );

    }
  }

  for my $t ( 1 .. $atu->scale_degrees - 1 ) {
    my $ips = $atu->transpose_invert( $ps, $t );
    my @i_invary;
    for my $p (@$ips) {
      push @i_invary, $p if exists $seen{$p};
    }
    if (@i_invary) {
      printf "%-5s [ %-${ps_len}s ] %s [ %-${ps_len}s ]\n", "Ti $t",
        join( ' ', map { $Flag_Lyout ? $lyu->p2ly($_) : $_ } @$ips ),
        'invars',
        join( ' ', map { $Flag_Lyout ? $lyu->p2ly($_) : $_ } @i_invary );
    }
  }
}

sub invert {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts, 'axis|n=s' => \my $axis, )
    || print_help();
  $lyu->chrome('flats') if $Flag_Flat;
  $axis //= 0;
  emit_pitch_set( $atu->invert( args2pitchset(@args), $axis ) );
}

sub multiply {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts, 'factor|n=s' => \my $factor, )
    || print_help();
  $lyu->chrome('flats') if $Flag_Flat;
  $factor //= 1;
  emit_pitch_set( $atu->multiply( args2pitchset(@args), $factor ) );
}

sub normal_form {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) || print_help();
  $lyu->chrome('flats') if $Flag_Flat;
  emit_pitch_set( $atu->normal_form( args2pitchset(@args) ) );
}

sub pcs2forte {
  my (@args) = @_;
  my $fn = $atu->pcs2forte( args2pitchset(@args) ) || "";
  print $fn, "\n";
}

sub pitch2intervalclass {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) || print_help();
  $lyu->chrome('flats') if $Flag_Flat;
  die "$0 pitch2intervalclass pitch\n"
    unless defined $args[0] and $args[0] =~ m/^\d+$/;
  print $atu->pitch2intervalclass( $args[0] );
}

sub prime_form {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) || print_help();
  $lyu->chrome('flats') if $Flag_Flat;
  emit_pitch_set( $atu->prime_form( args2pitchset(@args) ) );
}

sub print_help {
  warn <<"END_USAGE";
Usage: $0 [options] mode mode-args

Atonal music analysis utilities. Options:

  --flats               Show flats instead of sharps when --ly used.
  --help                Print this message.
  --listmodes           Show available modes (see Music::AtonalUtil for docs).
  --ly                  Show lilypond note names instead of pitch numbers.
  --scaledegrees=n      Set a custom number of scale degrees (default: 12).

Most modes accept a pitch set (a list of positive integers or lilypond
note names (see source for supported names)) either as arguments, or
specified on STDIN if the arguments list is blank, or the final argument
is a hyphen. Exceptions include:

  invert    --n=N       Custom inversion axis (default is 0).
  multiply  --n=N       Multiply the pitch set by a factor (default is 1).
  pitch2intervalclass   Accepts a single pitch, not a pitch set.
  transpose --n=N       Custom transposition (default is 0).

Forte Numbers should be usable anywhere a pitch set can be specified.
The output will vary depending on the mode, and may include Cope
tension numbers.

Example:
  $0 invert --axis=3  0 3 6 7

The following require two pitch sets; specify the pitch sets on STDIN
(one per line) instead of in the arguments:

  variances        Emits three lines: the intersection, the difference,
                   and the union of the supplied pitch sets.
  zrelation        Emits 1 if pitch sets zrelated, 0 if not.

Example:
  (echo 0,1,3,7; echo 0,1,4,6) | $0 zrelation

There is also a 'basic' mode that computes both the prime form and
interval class content (and Forte Number, if possible):

  $0 --ly basic c e g

Run perldoc(1) on this script for additional documentation.

END_USAGE
  exit 64;
}

sub recipe {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts, 'file=s' => \my $rfile )
    || print_help();
  $lyu->chrome('flats') if $Flag_Flat;

  my $ps  = args2pitchset(@args);
  my $wps = [@$ps];

  open my $fh, '<', $rfile or die "could not open '$rfile': $!\n";
  eval {
    while ( my ( $method, @margs ) = split ' ', readline $fh ) {
      next if !$method or $method =~ m/^[\s#]/;
      chomp @margs;
      die "not a ", ref $atu, " method" unless $atu->can($method);
      $wps = $atu->$method( $wps, @margs );
    }
  };
  if ($@) {
    chomp $@;
    die "recipe error at '$rfile' line $.: $@\n";
  }
  emit_pitch_set($wps);
}

sub retrograde {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) || print_help();
  $lyu->chrome('flats') if $Flag_Flat;
  emit_pitch_set( $atu->retrograde( args2pitchset(@args) ) );
}

sub rotate {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts, 'rotate|n=s' => \my $r, )
    || print_help();
  $lyu->chrome('flats') if $Flag_Flat;
  $r //= 0;
  emit_pitch_set( $atu->rotate( args2pitchset(@args), $r ) );
}

sub set_complex {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) || print_help();
  $lyu->chrome('flats') if $Flag_Flat;
  emit_pitch_set( $atu->set_complex( args2pitchset(@args) ) );
}

sub subsets {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts, 'length|len=i' => \my $l, )
    || print_help();
  $lyu->chrome('flats') if $Flag_Flat;
  emit_pitch_set( $atu->subsets( args2pitchset(@args), $l ) );
}

sub stdin2pitchsets {
  my ($atu) = @_;
  my $dis = $atu->scale_degrees;

  my @ss;
  while ( my $line = readline STDIN ) {
    my @pset;
    if ( $line =~ m/(\d-[zZ\d]+)/ ) {
      @pset = @{ $atu->forte2pcs($1) };
      die "unknown Forte Number '$1'\n" if !@pset;
    } else {
      for my $p ( $line =~ /([\d\w]+)/g ) {
        if ( $p =~ m/^\d+/ ) {
          push @pset, $p % $dis;
        } else {
          push @pset, $lyu->notes2pitches($p);
        }
      }
    }
    push @ss, \@pset;
  }

  return \@ss;
}

sub tcs {
  my (@args) = @_;
  emit_pitch_set( $atu->tcs( args2pitchset(@args) ), lyflag => 0 );
}

sub tcis {
  my (@args) = @_;
  emit_pitch_set( $atu->tcis( args2pitchset(@args) ), lyflag => 0 );
}

sub tension {
  my (@args) = @_;

  my ( $t_avg, $t_min, $t_max, $t_ref ) =
    $tension->vertical( args2pitchset(@args) );
  printf "%.03f  %.03f  %.03f\t%s\n", $t_avg, $t_min, $t_max,
    join( ',', @$t_ref );
}

sub transpose {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts, 'transpose|n=s' => \my $t, )
    || print_help();
  $lyu->chrome('flats') if $Flag_Flat;

  my $pset = args2pitchset(@args);

  $t //= 0;
  if ( !looks_like_number($t) ) {
    $t = $lyu->notes2pitches($t) - $pset->[0];
  }
  emit_pitch_set( $atu->transpose( $pset, $t ) );
}

sub transpose_invert {
  my (@args) = @_;
  GetOptionsFromArray(
    \@args, @Std_Opts,
    'axis|a=s'      => \my $axis,
    'transpose|t=s' => \my $t,
  ) || print_help();
  $lyu->chrome('flats') if $Flag_Flat;

  my $pset = args2pitchset(@args);

  $axis //= 0;
  if ( !looks_like_number($axis) ) {
    $axis = $lyu->notes2pitches($axis);
  }

  $t //= 0;
  if ( !looks_like_number($t) ) {
    $t = $lyu->notes2pitches($t) - $pset->[0];
  }

  emit_pitch_set( $atu->transpose_invert( $pset, $t, $axis ) );
}

sub variances {
  my (@args) = @_;
  GetOptionsFromArray( \@args, @Std_Opts ) || print_help();
  $lyu->chrome('flats') if $Flag_Flat;
  emit_pitch_set( [ $atu->variances( @{ stdin2pitchsets($atu) } ) ] );
}

sub zrelation {
  my ($atu) = @_;
  emit_pitch_set( [ $atu->zrelation( @{ stdin2pitchsets($atu) } ) ],
    lyflag => 0 );
}

END {
  # Report problems when writing to stdout (perldoc perlopentut)
  unless ( close(STDOUT) ) {
    die "error: problem closing STDOUT: $!\n";
  }
}

__END__

=head1 NAME

atonal-util - routines for atonal composition and analysis

=head1 SYNOPSIS

Prime form and APIC vector for a pitch set:

  $ atonal-util basic --ly f fis c

Apply a series of transformations to a pitch set:

  $ cat rules
  retrograde
  invert 6
  transpose 1
  $ atonal-util recipe --file=rules --ly 0 11 3
  4 8 7

=head1 DESCRIPTION

Routines for atonal music composition and analysis. Global options and
an operating mode should be supplied, followed by any mode specific
arguments and a pitch set. Pitch sets can be read as arguments or from
standard input; some modes require two pitch sets that must be supplied
one per line on standard input.

The output will vary depending on the mode, and may include Cope
tension numbers (average, min, max tension for the pitch set).

=head1 OPTIONS

This script currently supports the following global command line switches:

=over 4

=item B<--flats>

Uses flats instead of sharps in output (but only with B<--ly>).

=item B<--help>

Displays help and exits.

=item B<--listmodes>

Displays supported operation modes.

=item B<--ly>

Show lilypond note names instead of raw pitch numbers.

=item B<--scaledegrees>

Adjust the number of scale degrees (default: 12).

=back

=head1 MODES

Most all modes accept a pitch set (list of raw pitch numbers (0..number
of scale degrees) or lilypond note names (bis, c, des, etc.) either on
the command line or via standard input, though there are exceptions.

The global B<--ly> and B<--flats> can be specified as options to modes
that emit pitches. See also L<Music::AtonalUtil> for more documentation.

=over 4

=item B<basic> I<pitch_set>

Shows the B<prime_form> and B<interval_class_content>.

=item B<circular_permute>

See L<Music::AtonalUtil>.

=item B<complement>

See L<Music::AtonalUtil>.

=item B<equivs>

Equivalents under transposition and inverse transposition. An optional
axis of inversion (default: 0, though some forms use 6) can be supplied.

=item B<findall> I<[--exclude=p1[,p2]]> I<[--fn=nums]> I<[--root=pitch]> I<pitches>

Find all Forte pitch sets in which the given pitches exist. Like
B<findin>, except iterates over all Forte pitch sets instead of just
the given one.

With I<--exclude>, omits results containing the listed pitches. With I<--
fn>, limits the search to the mentioned forte number prefixes (the
number of pitches in the set). With I<--root>, limits matches to those
with the named root pitch.

  $ atonal-util findall --root=c --fn=4-5 c e g bes

Tensions may be lower than expected if the root pitch creates an open
position chord versus a closed position of that set.

=item B<findin> I<[--exclude=p1[,p2]]> I<[--root=pitch]> I<--pitchset=base_set> I<pitches>

Answers questions such as, given a base pitch set of C<[0,3,7]>, and the
notes d and bes, what pitch sets (via any B<transpose> or
B<transpose_invert> operation) complete the base pitch set. With I<--
exclude>, omits results containing the listed pitches. With I<--root>,
limits matches to those with the named root pitch.

  $ atonal-util findin --exclude=c,ees --pitchset=5-25 d fis a

Tensions may be lower than expected if the root pitch creates an open
position chord versus a closed position of that set.

=item B<forte2pcs> I<forte_number>

Given a Forte Number, return the corresponding pitch set.

=item B<forte2pcs> I<fnums>

Return a list of all Forte Numbers and corresponding pitch sets (and
their B<interval_class_content>), plus average tension, min tension, and
max tension via Music::Tension::Cope.

=item B<interval_class_content>

See L<Music::AtonalUtil>.

=item B<invariance_matrix>

See L<Music::AtonalUtil>.

=item B<invariants>

Returns list of B<transpose> or B<transpose_invert> operations that have
invariant pitches with the supplied pitch set, along with which pitches
have not varied.

=item B<invert> I<[--axis=inversion_axis]>

See L<Music::AtonalUtil>. Default axis is around 0.

=item B<multiply>

See L<Music::AtonalUtil>.

=item B<normal_form>

See L<Music::AtonalUtil>.

=item B<pcs2forte>

Given a pitch set, returns the corresponding Forte Number, if any.

=item B<pitch2intervalclass> I<pitch>

See L<Music::AtonalUtil>.

=item B<prime_form>

See L<Music::AtonalUtil>.

=item B<recipe> I<--file=recipefile>

Apply a series of named operations from a batch file to a pitch set,
for example:

  retrograde
  invert 6
  transpose 1

=item B<retrograde>

See L<Music::AtonalUtil>.

=item B<rotate> I<[--rotate=integer]>

See L<Music::AtonalUtil>.

=item B<set_complex>

See L<Music::AtonalUtil>.

=item B<subsets> I<[--length=integer]>

See L<Music::AtonalUtil>. The length, if supplied, must be a magnitude
equal to or less than the number of pitches supplied, and probably also
2 or higher.

=item B<tcis>

See L<Music::AtonalUtil>.

=item B<tcs>

See L<Music::AtonalUtil>.

=item B<tension> I<pitch_set>

Returns the average, min, max, and tension values for all the tensions
in the passed pitch set, from the first notes in the set up to the last.
(Via Music::Tension::Cope.)

=item B<transpose> I<--transpose=integer_or_note>

See L<Music::AtonalUtil>. Transposes the supplied pitches by the
specified integer (by default 0, or a no-op), or to the specified note.

  $ atonal-util transpose --transpose=4 --ly c e g
  e gis b
  $ atonal-util transpose --transpose=e --ly c e g
  e gis b

=item B<transpose_invert> I<--transpose=integer_or_note> I<[--axis=integer_or_note]>

See L<Music::AtonalUtil>. Transposes supplied pitch set by the specified
integer, or to the specified note. Default axis for inversion is 0 (c).

=item B<variances>

Accepts two pitch sets, one per line, via standard input.

=item B<zrelation>

Accepts two pitch sets, one per line, via standard input.

=back

=head1 FILES

ZSH completion script available in the zsh-compdef directory of the
L<App::MusicTools> distribution.

=head1 BUGS

=head2 Reporting Bugs

If the bug is in the latest version, send a report to the author.
Patches that fix problems or add new features are welcome.

http://github.com/thrig/App-MusicTools

=head2 Known Issues

Poor naming conventions and standards of underlying music theory and any
associated mistakes in understanding thereof by the author.

=head1 SEE ALSO

L<Music::AtonalUtil>, L<Music::LilyPondUtil>, L<Music::Tension::Cope>

=head1 AUTHOR

Jeremy Mates

=head1 COPYRIGHT

Copyright (C) 2012 by Jeremy Mates

This script is free software; you can redistribute it and/or modify it
under the same terms as Perl itself, either Perl version 5.16 or, at
your option, any later version of Perl 5 you may have available.

=cut
