App-MusicTools
view release on metacpan or search on metacpan
#!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 )