App-MusicTools

 view release on metacpan or  search on metacpan

bin/atonal-util  view on Meta::CPAN

#!perl
#
# atonal-util - command line interface to the atonal routines in the
# Music::AtonalUtil module (and otherwise a dumping ground for music
# related wrangling).
#
# Run perldoc(1) on this file for additional documentation.
#
# A ZSH completion script is available in the zsh-compdef/ directory of
# the App::MusicTools distribution.

# 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)
#
# (Or more likely scrap and rewrite from scratch, eesh.)

use 5.14.0;
use warnings;
use Getopt::Long         qw/GetOptionsFromArray/;
use List::Util           qw/sum/;
use Music::AtonalUtil    ();
use Music::LilyPondUtil  ();
use Music::Scala         ();
use Music::Scales        qw/get_scale_nums/;
use Music::Tempo         qw/bpm_to_ms ms_to_bpm/;
use Music::Tension::Cope ();
# untested with --tension
#use Music::Tension::PlompLevelt ();
use Parse::Range qw/parse_range/;
use Scalar::Util qw/looks_like_number/;

my %modes = (
    adjacent_interval_content => \&adjacent_interval_content,
    bark                      => \&bark,
    basic                     => \&basic,
    beats2set                 => \&beats2set,
    circular_permute          => \&circular_permute,
    combos                    => \&combos,
    complement                => \&complement,
    equivs                    => \&equivs,
    findall                   => \&findall,
    findin                    => \&findin,
    fnums                     => \&fnums,
    forte2pcs                 => \&forte2pcs,
    freq2pitch                => \&freq2pitch,
    gen_melody                => \&gen_melody,
    half_prime_form           => \&half_prime_form,
    interval_class_content    => \&interval_class_content,
    intervals2pcs             => \&intervals2pcs,
    invariance_matrix         => \&invariance_matrix,
    invariants                => \&invariants,
    invert                    => \&invert,
    ly2pitch                  => \&ly2pitch,
    ly2struct                 => \&ly2struct,
    multiply                  => \&multiply,
    normal_form               => \&normal_form,
    notes2time                => \&notes2time,
    pcs2forte                 => \&pcs2forte,
    pcs2intervals             => \&pcs2intervals,
    pitch2freq                => \&pitch2freq,
    pitch2intervalclass       => \&pitch2intervalclass,
    pitch2ly                  => \&pitch2ly,
    prime_form                => \&prime_form,
    recipe                    => \&recipe,
    retrograde                => \&retrograde,
    rotate                    => \&rotate,
    set2beats                 => \&set2beats,
    set_complex               => \&set_complex,
    subsets                   => \&subsets,
    tcis                      => \&tcis,
    tcs                       => \&tcs,
    tension                   => \&tension,
    time2notes                => \&time2notes,
    transpose                 => \&transpose,
    transpose_invert          => \&transpose_invert,
    variances                 => \&variances,
    whatscalesfit             => \&whatscalesfit,
    zrelation                 => \&zrelation,
);

my ( $Flag_Flat, $Flag_Lyout, $Flag_Quiet, $Flag_Tension );
my $Flag_Record_Sep = ',';    # mostly for pitch sets e.g. 0,4,7
my @Std_Opts        = (
    'flats!'    => \$Flag_Flat,
    'ly'        => \$Flag_Lyout,
    'quiet!'    => \$Flag_Quiet,
    'rs=s'      => \$Flag_Record_Sep,
    'tension=s' => \$Flag_Tension,
);

bin/atonal-util  view on Meta::CPAN

        warn "notice: incomplete beats ($i < $sd)\n" unless $Flag_Quiet;
    }

    my $lyify = $Flag_Lyout;
    if ( $sd != 12 ) {
        $lyify = 0;
    }
    emit_pitch_set( \@set, lyflag => $lyify, rs => $Flag_Record_Sep );
}

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

sub combos {
    my (@args) = @_;
    my $mode = 'absolute';
    GetOptionsFromArray(
        \@args, @Std_Opts,
        'concertfreq|cf=s'  => \my $concert_freq,
        'concertpitch|cp=s' => \my $concert_pitch,
        'pitches'           => \my $Flag_Pitches,
        'relative=s'        => \my $relative,
        'scala=s'           => \my $scala_file,
    ) or print_help();
    $Lyu->chrome('flats') if $Flag_Flat;

    $Lyu->ignore_register(0);
    $Lyu->keep_state(1);
    $Lyu->sticky_state(1);
    if ( defined $relative ) {
        if ( $relative =~ m/^\d+$/ ) {
            die "error: relative must be note name\n";
        }
        $Lyu->mode('relative');
        $Lyu->prev_note($relative);
    } else {
        $Lyu->mode('absolute');
    }

    my ( $scala, $p2f ) = _init_scala( $concert_freq, $concert_pitch, $scala_file );

    my @freqs;
    if ( !@args or ( @args == 1 and $args[0] eq '-' ) ) {
        while ( my $line = readline *STDIN ) {
            push @freqs, split ' ', $line;
        }
    } else {
        for my $arg (@args) {
            push @freqs, split ' ', $arg;
        }
    }

    if ( @freqs < 2 ) {
        die "Usage: $0 combos [--pitches [--relative=note]] f1 f2 [f3...]\n";
    }

    # turn on pitch mode if first note looks more a note than a number
    if ( $Flag_Pitches or $freqs[0] =~ m/[a-g]/ ) {
        @freqs = map $p2f->($_), $Lyu->notes2pitches(@freqs);
    }

    for my $i ( 1 .. $#freqs ) {
        my $plus  = $freqs[0] + $freqs[1];
        my $minus = $freqs[$i] - $freqs[0];

        # (try to) Figure out MIDI pitch of combination tone, and what the
        # error is due to presumed equal temperament tuning of said MIDI
        # pitches.
        my $plus_pitch  = 0;
        my $minus_pitch = 0;
        my $plus_delta  = 0;
        my $minus_delta = 0;
        my $errstr      = '';
        eval {
            $plus_pitch  = $scala->freq2pitch($plus);
            $minus_pitch = $scala->freq2pitch($minus);
            $plus_delta  = $p2f->($plus_pitch) - $plus;
            $minus_delta = $p2f->($minus_pitch) - $minus;
        };
        $errstr = "\t/!\\ pitch out of bounds" if $@;

        # best effort to get a note name, revert to pitch numbers if out of range
        if ( $Flag_Lyout and length $errstr == 0 ) {
            eval {
                $plus_pitch  = $Lyu->p2ly($plus_pitch);
                $minus_pitch = $Lyu->p2ly($minus_pitch);
            };
            $errstr = "\t/!\\ ly note out of bounds" if $@;
        }

        printf "%.2f+%.2f = %.2f\t(%s error %.2f)%s\n", $freqs[0], $freqs[$i],
          $plus,
          $plus_pitch, $plus_delta, $errstr;
        printf "%.2f-%.2f = %.2f\t(%s error %.2f)%s\n", $freqs[$i], $freqs[0],
          $minus,
          $minus_pitch, $minus_delta, $errstr;
    }
}

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

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 = exists $params{has_nl} ? $params{has_nl} : 0;
    my $str    = '';
    for my $i (@$pset) {
        if ( ref $i eq 'ARRAY' ) {
            $has_nl = emit_pitch_set( $i, %params );

bin/atonal-util  view on Meta::CPAN

            my $s = sprintf "%s\tTi(%d)\t%-${ps_width}s%s", $fnum, $i,
              join( ',', @pitches ), $tstr;
            $s =~ s/\s+$//;
            say $s;
        }
    }
}

sub fnums {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
    _init_tension('cope') if $Flag_Tension;

    my $fns = $Atu->fnums;
    for my $fn ( sort keys %$fns ) {
        my $pset = $fns->{$fn};
        my $icc  = $Atu->interval_class_content($pset);

        my $tstr = '';
        if ($Flag_Tension) {
            $tstr = sprintf "\t%.03f  %.03f  %.03f", $Tension->vertical($pset);
        }

        my $s = sprintf "%s\t%-16s\t%-8s%s", $fn, join( ',', @$pset ),
          join( '', @$icc ), $tstr;
        $s =~ s/\s+$//;
        say $s;
    }
}

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

    emit_pitch_set( $Atu->forte2pcs( $args[0] ), rs => $Flag_Record_Sep );
}

sub freq2pitch {
    my (@args) = @_;
    my $mode = 'absolute';
    GetOptionsFromArray(
        \@args,
        @Std_Opts,
        'concertfreq|cf=s'  => \my $concert_freq,
        'concertpitch|cp=s' => \my $concert_pitch,
        'relative=s'        => \my $relative,
        'scala=s'           => \my $scala_file,
    ) or print_help();

    my ( $scala, $p2f ) = _init_scala( $concert_freq, $concert_pitch, $scala_file );

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

    # Not the default, so if things persist or chain due to some rewrite,
    # would need to save the old or create a new object or whatever
    $Lyu->keep_state(1);
    $Lyu->mode('absolute');

    for my $freq ( grep looks_like_number $_, map { split ' ', $_ } @args ) {
        die "frequency '$freq' out of range" if $freq < 8 or $freq > 4200;

        my $p = $scala->freq2pitch($freq);

        # how off is the frequency from the given scale and concertfreq?
        my $pitch_freq = $scala->pitch2freq($p);
        my $error      = $freq - $pitch_freq;

        $p = $Lyu->p2ly($p) if $Flag_Lyout;

        my $percent = abs($error) / $pitch_freq * 100;

        printf "%.2f\t%s\t%+.2f\t%.2f%%\n", $freq, $p, $error, $percent;
    }
}

sub gen_melody {
    my (@args) = @_;
    $Flag_Record_Sep = ' ';    # for easier feeding to ly-fu
    GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
    $Lyu->chrome('flats') if $Flag_Flat;
    emit_pitch_set( $Atu->gen_melody, rs => $Flag_Record_Sep );
}

sub half_prime_form {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
    $Lyu->chrome('flats') if $Flag_Flat;
    emit_pitch_set( scalar $Atu->half_prime_form( args2pitchset(@args) ),
        rs => $Flag_Record_Sep );
}

sub interval_class_content {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts, 'scaledegrees|sd=i' => \my $sd );
    $Atu->scale_degrees($sd) if $sd;
    emit_pitch_set(
        scalar $Atu->interval_class_content( args2pitchset(@args) ),
        lyflag => 0,
        rs     => '',
    );
}

sub intervals2pcs {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts, 'pitch|n=s' => \my $start_pitch )
      or print_help();
    $start_pitch = $Lyu->notes2pitches( $start_pitch // 0 );

    $Lyu->ignore_register(0);

    emit_pitch_set( $Atu->intervals2pcs( $start_pitch, args2pitchset(@args) ),
        rs => $Flag_Record_Sep );
}

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

bin/atonal-util  view on Meta::CPAN

    my $beat_ms = bpm_to_ms( $tempo, $beats );
    my ( $scala, $p2f ) = _init_scala( $concert_freq, $concert_pitch, $scala_file );

    $Lyu->ignore_register(0);
    $Lyu->keep_state(1);
    $Lyu->sticky_state(1);
    if ( defined $relative ) {
        if ( $relative =~ m/^\d+$/ ) {
            die "error: relative must be note name\n";
        }
        $Lyu->mode('relative');
        $Lyu->prev_note($relative);
    } else {
        $Lyu->mode('absolute');
    }

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

    # split input, as lilypond ' really do not suit the Unix shell, so
    # are best enclosed in "" blocks
    for my $arg (@args) {
        push @notes, split ' ', $arg;
    }

    if ( !@notes ) {
        die "Usage: $0 ly2pitch [--relative=note] [-|notes...]\n";
    }

    my $prev_dur = $beat_ms / 4;
    for my $note (@notes) {
        my $pitch = $Lyu->notes2pitches($note);
        die "pitch '$pitch' out of range\n"
          if defined $pitch and ( $pitch < 0 or $pitch > 108 );
        my $duration;
        if (
            $note =~ m{ (?<dur>\d+) (?<dots>[.]+)? (?:\*(?<rnumer>\d+)/(?<rdenom>\d+))? }x )
        {
            my $dur   = $beats / $+{dur};
            my $dots  = exists $+{dots}   ? length $+{dots}         : 0;
            my $ratio = exists $+{rnumer} ? $+{rnumer} / $+{rdenom} : 1;
            $dur *= $ratio;
            $duration = ( 2 * $dur - ( $dur / 2**$dots ) ) * $beat_ms;
        }
        $duration //= $prev_dur;
        my $freq = ( defined $pitch and $pitch != 0 ) ? $p2f->($pitch) : 0;
        # zero precision due to Arduino tone call accepting integers
        printf "\t{ %.0f, %.0f },\t/* %s */\n", $freq, $duration, $note;
        $prev_dur = $duration;
    }
}

sub multiply {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts, 'factor|n=s' => \my $factor, )
      or print_help();
    $Lyu->chrome('flats') if $Flag_Flat;
    $factor //= 1;
    die "factor must be number\n" unless looks_like_number $factor;
    emit_pitch_set( $Atu->multiply( $factor, args2pitchset(@args) ) );
}

sub normal_form {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
    $Lyu->chrome('flats') if $Flag_Flat;
    emit_pitch_set( ( $Atu->normal_form( args2pitchset(@args) ) )[0],
        rs => $Flag_Record_Sep );
}

sub notes2time {
    my (@args)   = @_;
    my $beats    = 4;
    my $tempo    = 60;
    my $fraction = 1;
    GetOptionsFromArray(
        \@args,
        'beats=i'    => \$beats,
        'ms!'        => \my $in_ms,
        'tempo=i'    => \$tempo,
        'fraction=s' => \$fraction,
    ) or print_help();

    if ( !looks_like_number($fraction) ) {
        if ( $fraction =~ m#^(\d+)/(\d+)$# ) {
            $fraction = $1 / $2;
        } else {
            die "unknown fraction: $fraction (expect 2/3 or such)\n";
        }
    }

    my @durations;
    for my $notespec ( map { split ' ' } @args ) {
        if ( $notespec =~
            m{ (?<dur>\d+) (?<dots>[.]{1,10})? (?:\*(?<rnumer>\d+)/(?<rdenom>\d+))? }x ) {
            my $dur   = $beats / $+{dur};
            my $dots  = exists $+{dots}   ? length $+{dots}         : 0;
            my $ratio = exists $+{rnumer} ? $+{rnumer} / $+{rdenom} : 1;

            # Arbitrary limit to just nine dots, which is about three times longer
            # than I think I've ever seen in notation.
            die "error: too many dots in dotted note\n" if $dots > 9;

            $dur *= $fraction;
            $dur *= $ratio;
            push @durations, 2 * $dur - ( $dur / 2**$dots );
        } elsif ( !@durations ) {
            die "unable to parse duration from '$notespec'\n";
        } else {
            # assume repeated duration
            push @durations, $durations[-1];
        }
    }

    my $beat_ms = bpm_to_ms( $tempo, $beats );
    for my $d (@durations) {
        my $d_ms = $d * $beat_ms;
        print( ( $in_ms ? $d_ms : _ms2abbr_time($d_ms) ), "\n" );
    }

    if ( @durations > 1 ) {
        my $total_ms = sum(@durations) * $beat_ms;
        print( '= ' . ( $in_ms ? $total_ms : _ms2abbr_time($total_ms) ), "\n" );
    }
}

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

sub pcs2intervals {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts ) or print_help();

    $Lyu->ignore_register(0);

    emit_pitch_set(
        $Atu->pcs2intervals( args2pitchset(@args) ),
        lyflag => 0,
        rs     => $Flag_Record_Sep,
    );
}

sub pitch2freq {
    my (@args) = @_;
    my $emit = 1;
    if ( ref $args[0] ne '' ) {
        shift @args;
        $emit = 0;
    }
    my $mode = 'absolute';
    GetOptionsFromArray(
        \@args,
        'concertfreq|cf=s'  => \my $concert_freq,
        'concertpitch|cp=s' => \my $concert_pitch,
        'relative=s'        => \my $relative,
        'scala=s'           => \my $scala_file,
    ) or print_help();

    my ( $scala, $p2f ) = _init_scala( $concert_freq, $concert_pitch, $scala_file );

    $Lyu->ignore_register(0);
    $Lyu->keep_state(1);
    $Lyu->sticky_state(1);
    if ( defined $relative ) {
        if ( $relative =~ m/^\d+$/ ) {
            die "error: relative must be note name\n";
        }
        $Lyu->mode('relative');
        $Lyu->prev_note($relative);
    } else {
        $Lyu->mode('absolute');
    }

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

    # If pitch must int() it, otherwise feed to lilypond for conversion so
    # do not first need to call ly2pitch on the input.
    my @ret;
    for my $pitch (
        map { looks_like_number $_ ? int $_ : $Lyu->notes2pitches($_) }
        map { split ' ', $_ } @args
    ) {
        die "pitch '$pitch' out of range\n" if $pitch < 0 or $pitch > 108;
        if ($emit) {
            printf "%d\t%.2f\n", $pitch, $p2f->($pitch);
        } else {
            push @ret, $p2f->($pitch);
        }
    }
    return \@ret unless $emit;
}

sub pitch2intervalclass {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts ) or 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] ), "\n";
}

sub pitch2ly {
    my (@args) = @_;
    my $mode = 'absolute';
    GetOptionsFromArray( \@args, @Std_Opts, 'mode=s' => \$mode, )
      or print_help();

    # Not the default, so if things persist or chain due to some rewrite,
    # would need to save the old or create a new object or whatever
    $Lyu->keep_state(1);

    $Lyu->mode($mode) if defined $mode;

    my @pitches;
    if ( !@args or ( @args == 1 and $args[0] eq '-' ) ) {
        chomp( @args = readline *STDIN );
        for my $arg (@args) {
            push @pitches, split ' ', $arg;
        }
    } else {
        @pitches = @args;
    }

    print join( ' ', $Lyu->p2ly(@pitches) ), "\n";
}

sub prime_form {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
    $Lyu->chrome('flats') if $Flag_Flat;
    emit_pitch_set( $Atu->prime_form( args2pitchset(@args) ),
        rs => $Flag_Record_Sep );
}

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

Atonal music analysis utilities. Options:

bin/atonal-util  view on Meta::CPAN

  $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 atonal-util for additional documentation.

END_USAGE
    exit 64;
}

sub recipe {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts, 'file=s' => \my $rfile )
      or 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 $line = readline $fh ) {
            my ( $method, @margs ) = split ' ', $line;
            next if !$method or $method =~ m/^[\s#]/;
            chomp @margs;
            die "not a ", ref $Atu, " method" unless $Atu->can($method);
            $wps = $Atu->$method( @margs, $wps );
        }
    };
    die "recipe error at '$rfile' line $.: $@" if $@;
    emit_pitch_set( $wps, rs => $Flag_Record_Sep );
}

sub retrograde {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
    $Lyu->chrome('flats') if $Flag_Flat;
    emit_pitch_set( $Atu->retrograde( args2pitchset(@args) ),
        rs => $Flag_Record_Sep );
}

sub rotate {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts, 'rotate|n=s' => \my $r, )
      or print_help();
    $Lyu->chrome('flats') if $Flag_Flat;
    $r //= 0;
    die "rotate must be number\n" unless looks_like_number $r;
    emit_pitch_set( $Atu->rotate( $r, args2pitchset(@args) ),
        rs => $Flag_Record_Sep );
}

sub set2beats {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts, 'scaledegrees|sd=i' => \my $sd, )
      or print_help();
    if ($sd) {
        $Atu = Music::AtonalUtil->new( DEG_IN_SCALE => $sd );
    } else {
        $sd = $Atu->scale_degrees;
    }

    $Lyu->ignore_register(0) if $sd != 12;

    my $pset = args2pitchset(@args);
    my %set;
    @set{@$pset} = ();
    my $beats;
    for my $n ( 0 .. $sd - 1 ) {
        $beats .= exists $set{$n} ? 'x' : '.';
    }
    print $beats, "\n";
}

sub set_complex {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
    $Lyu->chrome('flats') if $Flag_Flat;
    emit_pitch_set( $Atu->set_complex( args2pitchset(@args) ),
        rs => $Flag_Record_Sep );
}

sub subsets {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts, 'length|len=i' => \my $l, )
      or print_help();
    $Lyu->chrome('flats') if $Flag_Flat;
    $l //= -1;
    emit_pitch_set( $Atu->subsets( $l, args2pitchset(@args) ),
        rs => $Flag_Record_Sep );
}

sub stdin2pitchsets {
    my @ss;
    while ( my $line = readline *STDIN ) {
        my @pset;
        if ( $line =~ m/($FORTE_NUMBER_RE)/ ) {
            @pset = @{ $Atu->forte2pcs($1) };
            die "unknown Forte Number '$1'\n" if !@pset;
        } else {
            for my $p ( $line =~ /([-\d\w]+)/g ) {
                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) = @_;
    GetOptionsFromArray( \@args, @Std_Opts ) or print_help();

    $Flag_Tension = 'cope' unless defined $Flag_Tension;
    _init_tension();

    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 time2notes {
    my (@args) = @_;
    my $beats  = 4;
    my $tempo  = 60;
    GetOptionsFromArray(
        \@args,
        'beats=i' => \$beats,
        'tempo=i' => \$tempo,
    ) or print_help();

    for my $durms (@args) {
        die "argument $durms not a number\n" if !looks_like_number $durms;
        my $dur = 1 / ( ms_to_bpm( $durms, $beats ) / $tempo );
        # Cheat with lilypond multiplier syntax; breaking things down into c4~ c16.
        # would of course be more work.
        $dur = 100 * sprintf "%.2f", $dur;
        print "c$beats*$dur/100\n";
    }

    if ( @args > 1 ) {
        my $total_ms = sum(@args);
        my $dur      = 1 / ( ms_to_bpm( $total_ms, $beats ) / $tempo );
        $dur = 100 * sprintf "%.2f", $dur;
        print "= c$beats*$dur/100\n";
    }
}

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

    my $pset = args2pitchset(@args);

    # if a number, transpose by that; if note, transpose to that note
    if ( !looks_like_number($t) ) {
        $t = $Lyu->notes2pitches($t) - $pset->[0];
    }
    emit_pitch_set( $Atu->transpose( $t, $pset ), rs => $Flag_Record_Sep );
}

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

    my $pset = args2pitchset(@args);

    $axis = defined $axis ? $Lyu->notes2pitches($axis) : 0;

    # if a number, transpose by that; if note, transpose to that note
    $t //= 0;
    if ( !looks_like_number($t) ) {
        $t = $Lyu->notes2pitches($t) - $pset->[0];
    }

    emit_pitch_set( $Atu->transpose_invert( $t, $axis, $pset ),
        rs => $Flag_Record_Sep );
}

sub variances {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
    $Lyu->chrome('flats') if $Flag_Flat;
    emit_pitch_set( [ $Atu->variances( @{ stdin2pitchsets() } ) ],
        rs => $Flag_Record_Sep );
}

sub whatscalesfit {
    my (@args) = @_;
    GetOptionsFromArray( \@args, @Std_Opts ) or print_help();
    $Lyu->chrome('flats') if $Flag_Flat;

    my $pset = args2pitchset(@args);
    for my $p (@$pset) {
        $p %= $scale_degrees;
    }

    my @scales = qw/major dorian phrygian lydian mixolydian aeolian locrian blues/;
    push @scales, "harmonic minor", "melodic minor", "hungarian minor";

    for my $scale (@scales) {
        my @asc = get_scale_nums($scale);
        _fit_scale( $scale, $pset, \@asc );

        my @dsc = get_scale_nums( $scale, 1 );
        if ( join( ' ', @asc ) ne join( ' ', reverse @dsc ) ) {
            _fit_scale( $scale, $pset, \@dsc, 1 );
        }
    }
}

sub zrelation {
    emit_pitch_set( [ $Atu->zrelation( @{ stdin2pitchsets() } ) ], 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:



( run in 1.424 second using v1.01-cache-2.11-cpan-d8267643d1d )