—#!perl
#
# scalemogrifier - generates notes of arbitrary scales (subsets of the
# Western 12-tone chromatic system) from a specified starting note in a
# specified direction and so forth from other options. This file uses
# "scale" and "subset" and "interval series" and "interval vector"
# interchangeably.
#
# 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.
use
5.14.0;
use
warnings;
use
Music::LilyPondUtil ();
# XXX but these have no concept of direction, which for some scales
# changes the notes, in particular:
#
# * mminor - melodic minor, should only be used ascending here.
#
# * amdorian - anti-melodic dorian, mixin from locrian, like melodic
# minor, except for downwards trending notes? Conventional voice
# leading tricky due to the diminished chords.
#
# XXX probably should standardize on Music::Scale (unmaintained?) or use
# Masaya Yamaguchi's complete scale system, plus offer ability for user
# to define new scales.
my
%modes
= (
ionian
=> [
qw(2 2 1 2 2 2 1)
],
lydian
=> [
qw(2 2 2 1 2 2 1)
],
mixolydian
=> [
qw(2 2 1 2 2 1 2)
],
dorian
=> [
qw(2 1 2 2 2 1 2)
],
amdorian
=> [
qw(2 1 2 1 2 2 2)
],
aeolian
=> [
qw(2 1 2 2 1 2 2)
],
mminor
=> [
qw(2 1 2 2 2 2 1)
],
hminor
=> [
qw(2 1 2 2 1 3 1)
],
hunminor
=> [
qw(2 1 3 2 1 3 1)
],
phrygian
=> [
qw(1 2 2 2 1 2 2)
],
locrian
=> [
qw(1 2 2 1 2 2 2)
],
'p-major'
=> [
qw(2 2 3 2 3)
],
'p-minor'
=> [
qw(3 2 2 3 2)
],
'p-sakura'
=> [
qw(1 4 2 1 4)
],
);
$modes
{
'major'
} =
$modes
{
'ionian'
};
$modes
{
'minor'
} =
$modes
{
'aeolian'
};
# Note names for program output
my
%num2note
= (
'ly-sharp'
=> {
qw/0 c 1 cis 2 d 3 dis 4 e 5 f 6 fis 7 g 8 gis 9 a 10 ais 11 b/
},
'ly-flat'
=> {
qw/0 c 1 des 2 d 3 ees 4 e 5 f 6 ges 7 g 8 aes 9 a 10 bes 11 b/
},
);
my
$num2note_flavor
=
'ly-'
;
# Not The Scale, but to map back to That Other Scale.
my
$DEG_IN_SCALE
= 12;
my
@interval_vector
= @{
$modes
{
'major'
} };
my
$length
= 8;
my
$direction
= 1;
my
$in_reverse
= 0;
my
$relative
= 0;
my
$transpose
;
my
$output_delimiter
=
" "
;
my
(
@output_intervals
,
$output_sum
);
my
$lyu
= Music::LilyPondUtil->new(
ignore_register
=> 1 );
########################################################################
#
# MAIN
GetOptions(
'direction=i'
=> \
$direction
,
'flats!'
=> \
my
$use_flats
,
'help|h|?'
=> \
&print_help
,
'intervals=s'
=> \
my
$intervals
,
'listmodes'
=> \
my
$list_modes
,
'minor'
=> \
my
$use_minor
,
'mode=s'
=> \
my
$mode
,
'ors=s'
=> \
$output_delimiter
,
'output_intervals|ois=s'
=> \
my
$output_intervals
,
'relative'
=> \
$relative
,
'length=i'
=> \
$length
,
'raw'
=> \
my
$raw_output
,
'reverse'
=> \
$in_reverse
,
'transpose|t=s'
=> \
$transpose
,
) or
die
"error: could not parse options\n"
;
print_help()
if
@ARGV
;
if
(
$list_modes
) {
"$_\n"
for
sort
keys
%modes
;
exit
0;
}
$direction
=
int
(
$direction
);
die
"error: a direction of zero is a bit too aimless\n"
if
$direction
== 0;
$length
=
int
(
$length
);
die
"error: length must be at least one\n"
if
$length
< 1;
$transpose
=
defined
$transpose
?
$lyu
->notes2pitches(
$transpose
) : 0;
$mode
=
'minor'
if
$use_minor
;
if
(
defined
$mode
) {
die
"error: no such mode '$mode'"
unless
exists
$modes
{
$mode
};
@interval_vector
= @{
$modes
{
$mode
} };
}
elsif
(
defined
$intervals
) {
@interval_vector
= names2intervalseries(
map
lc
,
split
/[\s,]+/,
$intervals
);
}
if
(
defined
$output_intervals
) {
my
@output_args
=
map
lc
,
split
/[\s,]+/,
$output_intervals
;
@output_intervals
= names2intervalseries(
@output_args
);
$output_sum
= sum
@output_intervals
;
}
$output_delimiter
=~ s/(\\.)/
qq!"$1"!
/eeg;
$num2note_flavor
.=
$use_flats
?
'flat'
:
'sharp'
;
@interval_vector
=
reverse
@interval_vector
if
$in_reverse
;
my
$pitch
= 0;
my
$prev_pitch
=
$pitch
;
for
my
$i
( 0 ..
$length
- 1 ) {
if
( !
$raw_output
) {
my
$output_pitch
=
$pitch
;
if
(
@output_intervals
) {
my
$pitch_reg
=
int
(
$output_pitch
/
@output_intervals
);
# XXX buggy
my
$pitch_mod
=
$output_pitch
%
@output_intervals
- 1;
$output_pitch
=
$pitch_reg
*
$output_sum
+
(
$pitch_mod
>= 0 ? sum(
@output_intervals
[ 0 ..
$pitch_mod
] ) : 0 );
}
my
$ly_reg
=
""
;
if
( !
$relative
and
$i
> 0 ) {
if
(
$output_pitch
< 0 ) {
$ly_reg
=
q{,}
x ( (
abs
(
$output_pitch
) +
$DEG_IN_SCALE
- 1 ) /
$DEG_IN_SCALE
);
}
elsif
(
$output_pitch
>=
$DEG_IN_SCALE
) {
$ly_reg
=
q{'}
x (
$output_pitch
/
$DEG_IN_SCALE
);
}
}
$num2note
{
$num2note_flavor
}
->{ (
$output_pitch
+
$transpose
) %
$DEG_IN_SCALE
},
$ly_reg
;
}
else
{
$pitch
+
$transpose
;
}
$output_delimiter
if
$i
<
$length
- 1;
$prev_pitch
=
$pitch
;
$pitch
+=
$interval_vector
[
$i
%
@interval_vector
] *
$direction
;
}
"\n"
;
exit
0;
########################################################################
#
# SUBROUTINES
# Convert between input of 'c cis dis e g' to an interval series, so
# user can input arbitrary scales using note names. Mixing note names
# with interval numbers is not supported in this implementation.
sub
names2intervalseries {
my
@input
=
@_
;
my
@iseries
;
my
$is_conversion
= 0;
my
$is_complete_octave
= 1;
if
(
$input
[-1] eq
'nl'
) {
$is_complete_octave
= 0;
pop
@input
;
}
my
@indexes
= 0 ..
$#input
;
push
@indexes
, 0
if
$is_complete_octave
;
my
$prev_pitch
;
for
my
$i
(
@indexes
) {
if
(
$input
[
$i
] !~ m/^-?\d+$/ ) {
$is_conversion
= 1;
if
(
defined
$prev_pitch
) {
my
$delta
=
$lyu
->notes2pitches(
$input
[
$i
] ) -
$prev_pitch
;
if
(
abs
(
$delta
) >
$DEG_IN_SCALE
/ 2 ) {
$delta
= (
$DEG_IN_SCALE
-
abs
(
$delta
) ) * (
$delta
> 0 ? -1 : 1 );
}
push
@iseries
,
$delta
;
}
$prev_pitch
=
$lyu
->notes2pitches(
$input
[
$i
] );
}
}
@iseries
=
@input
unless
$is_conversion
;
return
@iseries
;
}
sub
print_help {
$Text::Wrap::columns
= 78;
my
$modes
= wrap(
' '
,
' '
,
sort
keys
%modes
);
warn
<<"END_HELP";
Usage: $0 [options]
--listmodes Shows list of available modes.
Options affecting input:
--direction Positive, scale goes up, negative, down. Magnitudes
greater than one multiply the interval.
--intervals List of intervals or notes of the scale.
--length How many notes to generate in output.
--mode Specify named interval series instead of --intervals.
--minor Use minor mode instead of --intervals (default: major).
--reverse Reverses order of intervals.
Options affecting output:
--flats Use flats in output, instead of default sharps.
--ois Custom scale or interval set output will be mapped into.
--ors What will be printed between output elements.
Defaults to the space character.
--raw Emit pitch numbers instead of note names.
--relative Generate relative lilypond output (default: absolute).
--transpose Number to transpose the output by.
Available modes for --modes option:
$modes
END_HELP
exit
64;
}
END {
# Report problems when writing to stdout (perldoc perlopentut)
unless
(
close
(STDOUT) ) {
warn
"error: problem closing STDOUT: $!\n"
;
exit
74;
}
}
__END__
=head1 NAME
scalemogrifier - generate notes of named or arbitrary scales
=head1 SYNOPSIS
$ scalemogrifier
c d e f g a b c'
The list of accepted lilypond note names and other elements include:
c cis des d dis ees e f fis ges g gis aes a ais bes b nl
Available named modes include:
aeolian amdorian dorian hminor hunminor ionian locrian lydian
major minor mixolydian mminor phrygian
=head1 DESCRIPTION
Generates notes of arbitrary scales (subsets of the Western 12-tone
chromatic system) from a specified starting note in a specified
direction and so forth from other options.
The output is in lilypond absolute format. The default scale is The
Major Scale, but that's easy to adjust to say C Minor:
$ scalemogrifier --minor --flats
By default, scales loop back to the starting note. If this is not the
case, suffix C<nl> to a custom interval series:
$ scalemogrifier --intervals=c,cis,dis,e,g,nl --length=15
c cis dis e g gis ais b d' dis' f' fis' a' ais' c''
The output is based on the assumption that C<c> equals 0. The output
can be transposed via:
$ scalemogrifier --mode=mixolydian --transpose=-5
Integer interval numbers can be used instead of note names or the
--mode option, for example the Major Scale:
$ scalemogrifier --intervals='2 2 1 2 2 2 1' --raw
Or to form a -P4,+P5 sequence:
$ scalemogrifier --intervals=-5,7 --len=24
Or just plain made up:
$ scalemogrifier --intervals=1,2,3,5,7 --dir=53 --relative --len=24
Scales can also be rendered backwards:
$ scalemogrifier --dir=-1
Or with the interval order reversed:
$ scalemogrifier --rev
The scale generated can be fed back into a different output scale (the
direction and reverse options only apply to the --intervals option
scale, not the --ois scale):
$ scalemogrifier --intervals=c,cis,dis,e,g,nl --ois=aes,bes,ces,ees,fes \
--len=48 --flats --rel
Practical application of the results to music theory or composition left
as an exercise to the user. The output can be fed to C<ly-fu> for
playback and display, for example:
$ ly-fu --abs -l --open $(scalemogrifier ...)
$ ly-fu -l --open $(scalemogrifier --relative ...)
=head1 OPTIONS
This program currently supports the following command line switches:
=over 4
=item B<--direction>=I<integer>
Positive, the scale goes up, negative, down. Magnitudes greater than 1
multiply the interval.
=item B<--flats> | B<--noflats>
Use flats instead of sharps in the output note names. Prefix with no to
disable flats, in the event an alias has set them on by default.
=item B<--help>
Displays help and exits program.
=item B<--intervals>=I<interval list>
List (comma or space delimited) of intervals (integers) or notes of
the scale (lilypond note names) or C<nl> for non-loop-back-to-start-
pitch scales.
=item B<--length>=I<positive integer>
How many notes to generate in the output.
=item B<--listmodes>
List available named scale modes and then exit. Used by ZSH
compdef script.
=item B<--mode>=I<mode>
Specify named scale mode instead of using B<--intervals>. Available
modes can be listed with the B<--listmodes> option.
=item B<--ois>=I<interval list>
"Output interval sequence" -- the custom scale or interval set that the
output will be mapped into.
=item B<--ors>=I<string>
"Output record separator" -- text to print between each output element.
Defaults to a space character.
=item B<--raw>
Emit raw pitch numbers instead of note names.
=item B<--relative>
Generate relative lilypond output (default: absolute). Good fun can be
had by using absolute in relative mode, or otherwise letting the chance
of the lilypond algorithm take the music in unexpected directions.
=item B<--reverse>
Reverses the order of the intervals.
=item B<--transpose>=I<pitch or note>
Value by which to transpose the output by (integer) or to (note name).
=back
=head1 FILES
A ZSH completion script is available in the C<zsh-compdef/> directory of
the L<App::MusicTools> distribution. Install this to a C<$fpath>
directory.
=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.
=head2 Known Issues
None at this time but probably lots.
=head1 SEE ALSO
Consider L<http://oeis.org/> to lookup interesting interval series; for
example, search by the major interval sequence for numeric sequences
that contain it: L<http://oeis.org/search?q=2,2,1,2,2,2,1>
L<Music::Canon> contains routines for mapping series of pitches via
arbitrary scales or interval sequences to produce new material. See also
C<canonical> of this distribution.
=head1 COPYRIGHT
Copyright 2012 Jeremy Mates
This program is distributed under the (Revised) BSD License:
=cut