App-MusicTools

 view release on metacpan or  search on metacpan

bin/vov  view on Meta::CPAN

#!perl
#
# vov - musical V/V utility, for arbitrary "chord X relative to Y as
# tonic in key Z" type calculations (V/V are called "applied dominants"
# among various other names). Any scale degree can be used, e.g. the
# phrase with a sub-phrase relative to IV "I IV/IV V/IV IV V I" says
# tonic (C), subdominant of the subdominant (B of F), dominant of the
# subdominant (C of F), and then subdominant (F), dominant (G) and back
# to the tonic.
#
# 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 the code is something of a mess, and should be simplified and
# rewritten. But that takes time and energy.

use 5.14.0;
use warnings;
use Carp;
use Getopt::Long qw/GetOptions/;
use List::Util   qw/sum/;
# So as it turns out atonal functions can be handy for various
# tonal needs.
use Music::AtonalUtil   ();
use Music::LilyPondUtil ();
use Music::Scales       qw/get_scale_nums is_scale/;
use Text::Roman         qw/roman2int/;

# Leading sharp/flats adjust the tonic up or down (might also support
# doublesharp or doubleflat, but those get tricky depending on the
# underlying note, and are not used in output (e.g. 'a' will be shown
# instead of a 'beses' for a double-diminished 7th).
my $FLAT_CHARS      = 'b\x{266D}';
my $SHARP_CHARS     = '#\x{266F}';
my $ROMAN_PREFIX_RE = qr/[$FLAT_CHARS$SHARP_CHARS]/;

# Upper vs. lower case indicates major vs. minor quality of the 3rd
my $ROMAN_NUMERAL_RE = qr/III|iii|VII|vii|II|ii|IV|iv|VI|vi|I|i|V|v/;

# Whether to aug or dim or double dim the chord (+ * **), the chord
# factor or inversion data, or inversion by letter form.
my $ROMAN_SUFFIX_RE = qr/[+*]?[*]?\d*[a-g]?/;

# Standard tonal limits on scale degrees and non-repetition of triad
# pitches; adjust these if using some other scale system.
my $MAX_SCALE_DEGREE = 7;
# Western system of 7 scale degrees allows for at most a 13th chord
# before repeats; the following generalizes to arbitrary degrees.
my $MAX_CHORD_FACTOR =
  ( $MAX_SCALE_DEGREE % 2 == 0 ? $MAX_SCALE_DEGREE : $MAX_SCALE_DEGREE * 2 ) -
  1;
my $DEFAULT_CHORD_FACTOR = 5;    # a 5th
my $DEG_IN_SCALE         = 12;
my @MODES =
  qw/aeolian amdorian dorian hminor hunminor ionian locrian lydian major minor mixolydian mminor phrygian/;
my $mode_name = 'major';

my $atu = Music::AtonalUtil->new;
# Chords generated from the root up, with no notion of register
my $lyu = Music::LilyPondUtil->new( ignore_register => 1, keep_state => 0 );

my $output_tmpl = '%{chord}' . "\n";

GetOptions(
    'factor=i'      => \my $Default_Factor,
    'flats!'        => \my $use_flats,
    'help'          => \&print_help,
    'listmodes'     => \my $list_modes,
    'minor'         => \my $use_minor,
    'mode=s'        => \$mode_name,
    'natural'       => \my $use_naturals,
    'outputtmpl=s'  => \$output_tmpl,
    'raw'           => \my $raw_output,
    'transpose|t=s' => \my $Transpose,
) or print_help();

if ($list_modes) {
    print "$_\n" for @MODES;
    exit 0;
}
print_help() unless @ARGV;

$lyu->chrome('flats') if $use_flats;

$mode_name = 'minor' if $use_minor;
die "error: no such mode '$mode_name'" unless is_scale $mode_name;

$Default_Factor //= $DEFAULT_CHORD_FACTOR;
$Transpose = $lyu->notes2pitches($Transpose) if $Transpose;

$output_tmpl =~ s/(\\.)/qq!"$1"!/eeg;
$output_tmpl .= "\n" unless $output_tmpl =~ m/\s$/;

for my $vov_spec (@ARGV) {
    my @vovs           = reverse split '/', $vov_spec;
    my $base_intervals = get_mode_intervals($mode_name);
    my $cur_intervals  = $base_intervals;
    my $sd_transpose   = 0;

    my ( $prev_root_pitch, $prev_root_sd, $pset, $invert_by );
    for my $vov (@vovs) {
        my ( $root_sd, $factor, $alterations, $inv ) = parse_roman_numeral($vov);
        if ( defined $prev_root_sd ) {
            $sd_transpose = $prev_root_pitch;
            # This rotation trick constrains the pitches of the new relative
            # pitch to those of the overlying mode, as otherwise III/ii
            # assuming major will use pitches not present in the underlying
            # major scale.
            $cur_intervals = $atu->rotate( -1 * $prev_root_sd, $base_intervals );
        }
        my $sds = build_triad_degrees( $root_sd, $factor );
        $pset = sd2ps( $sds, $alterations, $cur_intervals, $sd_transpose );

        $prev_root_pitch = $pset->[0];
        $prev_root_sd    = $root_sd;



( run in 0.804 second using v1.01-cache-2.11-cpan-98e64b0badf )