—package
Music::Chord::Progression;
our
$AUTHORITY
=
'cpan:GENE'
;
# ABSTRACT: Create network transition chord progressions
our
$VERSION
=
'0.0609'
;
use
Moo;
use
strictures 2;
use
Graph::Directed ();
use
Music::Chord::Note ();
use
namespace::clean;
has
max
=> (
is
=>
'ro'
,
isa
=>
sub
{ croak
"$_[0] is not a valid integer"
unless
$_
[0] =~ /^\d+$/ },
default
=>
sub
{ 8 },
);
has
net
=> (
is
=>
'ro'
,
isa
=>
sub
{ croak
"$_[0] is not a hashref"
unless
ref
$_
[0] eq
'HASH'
},
default
=>
sub
{
{
1
=> [
qw( 1 2 3 4 5 6 )
],
2
=> [
qw( 3 4 5 )
],
3
=> [
qw( 1 2 4 6 )
],
4
=> [
qw( 1 3 5 6 )
],
5
=> [
qw( 1 4 6 )
],
6
=> [
qw( 1 2 4 5 )
],
7
=> [] }
},
);
has
chord_map
=> (
is
=>
'lazy'
,
);
sub
_build_chord_map {
my
(
$self
) =
@_
;
my
%scale
= (
chromatic
=> [ (
'm'
) x 12 ],
major
=> [
''
,
'm'
,
'm'
,
''
,
''
,
'm'
,
'dim'
],
ionian
=> [
''
,
'm'
,
'm'
,
''
,
''
,
'm'
,
'dim'
],
dorian
=> [
'm'
,
'm'
,
''
,
''
,
'm'
,
'dim'
,
''
],
phrygian
=> [
'm'
,
''
,
''
,
'm'
,
'dim'
,
''
,
'm'
],
lydian
=> [
''
,
''
,
'm'
,
'dim'
,
''
,
'm'
,
'm'
],
mixolydian
=> [
''
,
'm'
,
'dim'
,
''
,
'm'
,
'm'
,
''
],
minor
=> [
'm'
,
'dim'
,
''
,
'm'
,
'm'
,
''
,
''
],
aeolian
=> [
'm'
,
'dim'
,
''
,
'm'
,
'm'
,
''
,
''
],
locrian
=> [
'dim'
,
''
,
'm'
,
'm'
,
''
,
''
,
'm'
],
);
return
$scale
{
$self
->scale_name };
}
has
scale_name
=> (
is
=>
'ro'
,
isa
=>
sub
{ croak
"$_[0] is not a valid string"
if
ref
$_
[0] },
default
=>
sub
{
'major'
},
);
has
scale_note
=> (
is
=>
'ro'
,
isa
=>
sub
{ croak
"$_[0] is not a valid note"
unless
$_
[0] =~ /^[A-G][
#b]?$/ },
default
=>
sub
{
'C'
},
);
has
scale
=> (
is
=>
'lazy'
,
init_args
=>
undef
,
);
sub
_build_scale {
my
(
$self
) =
@_
;
my
@scale
= get_scale_notes(
$self
->scale_note,
$self
->scale_name);
my
%equiv
= (
'C#'
=>
'Db'
,
'D#'
=>
'Eb'
,
'E#'
=>
'F'
,
'F#'
=>
'Gb'
,
'G#'
=>
'Ab'
,
'A#'
=>
'Bb'
,
'B#'
=>
'C'
,
'Cb'
=>
'B'
,
'Dbb'
=>
'C'
,
'Ebb'
=>
'D'
,
'Fb'
=>
'E'
,
'Gbb'
=>
'F'
,
'Abb'
=>
'G'
,
'Bbb'
=>
'A'
,
);
for
(
@scale
) {
$_
=
$equiv
{
$_
}
if
exists
$equiv
{
$_
};
}
ucfirst
(
$self
->scale_name),
' scale: '
, ddc(\
@scale
)
if
$self
->verbose;
return
\
@scale
;
}
has
octave
=> (
is
=>
'ro'
,
isa
=>
sub
{ croak
"$_[0] is not a valid octave"
unless
$_
[0] =~ /^-?\d+$/ },
default
=>
sub
{ 4 },
);
has
tonic
=> (
is
=>
'ro'
,
isa
=>
sub
{ croak
"$_[0] is not a valid setting"
unless
$_
[0] =~ /^-?[01]$/ },
default
=>
sub
{ 1 },
);
has
resolve
=> (
is
=>
'ro'
,
isa
=>
sub
{ croak
"$_[0] is not a valid setting"
unless
$_
[0] =~ /^-?[01]$/ },
default
=>
sub
{ 1 },
);
has
substitute
=> (
is
=>
'ro'
,
isa
=>
sub
{ croak
"$_[0] is not a valid boolean"
unless
$_
[0] =~ /^[01]$/ },
default
=>
sub
{ 0 },
);
has
sub_cond
=> (
is
=>
'ro'
,
isa
=>
sub
{ croak
"$_[0] is not a valid coderef"
unless
ref
(
$_
[0]) eq
'CODE'
},
default
=>
sub
{
return
sub
{
int
rand
4 == 0 } },
);
has
flat
=> (
is
=>
'ro'
,
isa
=>
sub
{ croak
"$_[0] is not a valid boolean"
unless
$_
[0] =~ /^[01]$/ },
default
=>
sub
{ 0 },
);
has
graph
=> (
is
=>
'lazy'
,
init_args
=>
undef
,
);
sub
_build_graph {
my
(
$self
) =
@_
;
my
$g
= Graph::Directed->new;
for
my
$posn
(
keys
%{
$self
->net }) {
for
my
$p
(@{
$self
->net->{
$posn
} }) {
$g
->add_edge(
$posn
,
$p
);
}
}
return
$g
;
}
has
phrase
=> (
is
=>
'rw'
,
init_args
=>
undef
,
);
has
chords
=> (
is
=>
'rw'
,
init_args
=>
undef
,
);
has
verbose
=> (
is
=>
'ro'
,
isa
=>
sub
{ croak
"$_[0] is not a valid boolean"
unless
$_
[0] =~ /^[01]$/ },
default
=>
sub
{ 0 },
);
sub
generate {
my
(
$self
) =
@_
;
croak
'chord_map length must equal number of net keys'
unless
@{
$self
->chord_map } ==
keys
%{
$self
->net };
'Graph: '
.
$self
->graph,
"\n"
if
$self
->verbose;
# Create a random progression
my
@progression
;
my
$v
;
# Vertex
for
my
$n
(1 ..
$self
->max) {
$v
=
$self
->_next_successor(
$n
,
$v
);
push
@progression
,
$v
;
}
'Progression: '
, ddc(\
@progression
)
if
$self
->verbose;
my
@chord_map
= @{
$self
->chord_map };
if
(
$self
->substitute) {
my
$i
= 0;
for
my
$chord
(
@chord_map
) {
my
$substitute
=
$self
->sub_cond->() ?
$self
->substitution(
$chord
) :
$chord
;
if
(
$substitute
eq
$chord
&&
$i
<
@progression
&&
$self
->sub_cond->()) {
$progression
[
$i
] .=
't'
;
# Indicate that we should tritone substitute
}
$chord
=
$substitute
;
$i
++;
}
}
'Chord map: '
, ddc(\
@chord_map
)
if
$self
->verbose;
my
@phrase
=
map
{
$self
->_tt_sub(\
@chord_map
,
$_
) }
@progression
;
$self
->phrase(\
@phrase
);
'Phrase: '
, ddc(
$self
->phrase)
if
$self
->verbose;
# Add octaves to the chords
my
$mcn
= Music::Chord::Note->new;
my
@chords
;
for
my
$chord
(
@phrase
) {
my
@chord
=
$mcn
->chord_with_octave(
$chord
,
$self
->octave);
push
@chords
, \
@chord
;
}
if
(
$self
->flat) {
my
%equiv
= (
'C#'
=>
'Db'
,
'D#'
=>
'Eb'
,
'E#'
=>
'F'
,
'F#'
=>
'Gb'
,
'G#'
=>
'Ab'
,
'A#'
=>
'Bb'
,
'B#'
=>
'C'
,
);
for
my
$chord
(
@chords
) {
for
my
$note
(
@$chord
) {
$note
=~ s/^([A-G]
#)(\d+)$/$equiv{$1}$2/ if $note =~ /#/;
}
}
}
$self
->chords(\
@chords
);
'Chords: '
, ddc(
$self
->chords)
if
$self
->verbose;
return
\
@chords
;
}
sub
_next_successor {
my
(
$self
,
$n
,
$v
) =
@_
;
$v
//= 1;
my
$s
;
if
(
$n
== 1) {
if
(
$self
->tonic == 0) {
$s
=
$self
->graph->random_successor(1);
}
elsif
(
$self
->tonic == 1) {
$s
= 1;
}
else
{
$s
=
$self
->_full_keys;
}
}
elsif
(
$n
==
$self
->max) {
if
(
$self
->resolve == 0) {
$s
=
$self
->graph->random_successor(
$v
) ||
$self
->_full_keys;
}
elsif
(
$self
->resolve == 1) {
$s
= 1;
}
else
{
$s
=
$self
->_full_keys;
}
}
else
{
$s
=
$self
->graph->random_successor(
$v
);
}
return
$s
;
}
sub
_full_keys {
my
(
$self
) =
@_
;
my
@keys
=
grep
{
keys
@{
$self
->net->{
$_
} } > 0 }
keys
%{
$self
->net };
return
$keys
[
int
rand
@keys
];
}
sub
_tt_sub {
my
(
$self
,
$chord_map
,
$n
) =
@_
;
my
$note
;
if
(
$n
=~ /t/) {
my
@fnotes
= get_scale_notes(
'C'
,
'chromatic'
, 0,
'b'
);
my
@snotes
= get_scale_notes(
'C'
,
'chromatic'
);
my
%ftritone
=
map
{
$fnotes
[
$_
] =>
$fnotes
[(
$_
+ 6) %
@fnotes
] } 0 ..
$#fnotes
;
my
%stritone
=
map
{
$snotes
[
$_
] =>
$snotes
[(
$_
+ 6) %
@snotes
] } 0 ..
$#snotes
;
$n
=~ s/t//;
$note
=
$ftritone
{
$self
->scale->[
$n
- 1] } ||
$stritone
{
$self
->scale->[
$n
- 1] };
'Tritone: '
,
$self
->scale->[
$n
- 1],
" => $note\n"
if
$self
->verbose;
}
else
{
$note
=
$self
->scale->[
$n
- 1];
}
$note
.=
$chord_map
->[
$n
- 1];
"Note: $note\n"
if
$self
->verbose;
return
$note
;
}
sub
substitution {
my
(
$self
,
$chord
) =
@_
;
my
$substitute
=
$chord
;
if
(
$chord
eq
''
||
$chord
eq
'm'
) {
my
$roll
=
int
rand
2;
$substitute
=
$roll
== 0 ?
$chord
.
'M7'
:
$chord
. 7;
}
elsif
(
$chord
eq
'dim'
||
$chord
eq
'aug'
) {
$substitute
=
$chord
. 7;
}
elsif
(
$chord
eq
'-5'
||
$chord
eq
'-9'
) {
$substitute
=
"7($chord)"
;
}
elsif
(
$chord
eq
'M7'
) {
my
$roll
=
int
rand
3;
$substitute
=
$roll
== 0 ?
'M9'
:
$roll
== 1 ?
'M11'
:
'M13'
;
}
elsif
(
$chord
eq
'7'
) {
my
$roll
=
int
rand
3;
$substitute
=
$roll
== 0 ?
'9'
:
$roll
== 1 ?
'11'
:
'13'
;
}
elsif
(
$chord
eq
'm7'
) {
my
$roll
=
int
rand
3;
$substitute
=
$roll
== 0 ?
'm9'
:
$roll
== 1 ?
'm11'
:
'm13'
;
}
qq|Substitute: "$chord" => "$substitute"\n|
if
$self
->verbose &&
$substitute
ne
$chord
;
return
$substitute
;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Music::Chord::Progression - Create network transition chord progressions
=head1 VERSION
version 0.0609
=head1 SYNOPSIS
use Music::Chord::Progression;
use MIDI::Util qw(setup_score midi_format);
my $prog = Music::Chord::Progression->new;
my $chord_type = $prog->substitution('m'); # m7 or mM7
my $progression = $prog->generate;
my $score = setup_score();
for my $chord (@$progression) {
$score->n('wn', midi_format(@$chord));
}
=head1 DESCRIPTION
C<Music::Chord::Progression> creates network transition chord
progressions.
This module can also perform limited jazz chord substitutions, if
requested in the constructor.
=head1 ATTRIBUTES
=head2 max
The number of chords to generate in a phrase.
Default: C<8>
=head2 net
The network transitions between chords of the progression.
The keys must start with C<1> and be contiguous to the end.
Ending on C<12> keys all the notes of the chromatic scale. Ending on
C<7> represents diatonic notes, given the B<scale_name>.
If you do not wish a scale note to be chosen, include it among the
keys, but do not refer to it and do not give it any neighbors. Thus,
in the first example, the 7th degree of the scale will never be chosen.
Default:
{ 1 => [qw( 1 2 3 4 5 6 )],
2 => [qw( 3 4 5 )],
3 => [qw( 1 2 4 6 )],
4 => [qw( 1 3 5 6 )],
5 => [qw( 1 4 6 )],
6 => [qw( 1 2 4 5 )],
7 => [] }
A contrived chromatic example where each note connects to every note:
{ 1 => [1 .. 12],
2 => [1 .. 12],
3 => [1 .. 12],
4 => [1 .. 12],
5 => [1 .. 12],
6 => [1 .. 12],
7 => [1 .. 12],
8 => [1 .. 12],
9 => [1 .. 12],
10 => [1 .. 12],
11 => [1 .. 12],
12 => [1 .. 12],
}
=head2 chord_map
The chord names of each scale position.
The number of items in this list must be equal to the number of keys
in the B<net>.
Default: C<[ '', 'm', 'm', '', '', 'm', 'dim' ]> (major scale)
Here C<''> refers to the major chord and C<'m'> means minor.
Here are the known chord mappings:
chromatic => [ ('m') x 12 ],
major => [ '', 'm', 'm', '', '', 'm', 'dim' ],
ionian => [ '', 'm', 'm', '', '', 'm', 'dim' ],
dorian => [ 'm', 'm', '', '', 'm', 'dim', '' ],
phrygian => [ 'm', '', '', 'm', 'dim', '', 'm' ],
lydian => [ '', '', 'm', 'dim', '', 'm', 'm' ],
mixolydian => [ '', 'm', 'dim', '', 'm', 'm', '' ],
minor => [ 'm', 'dim', '', 'm', 'm', '', '' ],
aeolian => [ 'm', 'dim', '', 'm', 'm', '', '' ],
locrian => [ 'dim', '', 'm', 'm', '', '', 'm' ],
If B<chord_map> is not defined and a known B<scale_name> is given to
the constructor, the corresponding B<chord_map> above, will be used.
Alternative example:
[ 'M7', 'm7', 'm7', 'M7', '7', 'm7', 'dim7' ]
The known chord names are listed in the source of L<Music::Chord::Note>.
=head2 scale_name
The name of the scale.
Default: C<major>
Please see L<Music::Scales/SCALES> for the allowed scale names.
=head2 scale_note
The (uppercase) name of the scale starting note with an optional C<#>
or C<b> accidental.
Default: C<C>
=head2 scale
The scale notes. This is a computed attribute.
Default: C<[C D E F G A B]>
=head2 octave
The octave of the scale.
Default: C<4>
=head2 tonic
Set the start of the progression.
If this is given as C<1> the tonic chord starts the progression. If
given as C<0> a neighbor of the tonic is chosen. If given as C<-1> a
random B<net> key is chosen.
Default: C<1>
=head2 resolve
Set the end of the progression.
If this is given as C<1> the tonic chord ends the progression. If
given as C<0> a neighbor of the last chord is chosen. If given as
C<-1> a random B<net> key is chosen.
Default: C<1>
=head2 substitute
Perform jazz chord substitution.
Default: C<0>
Rules:
=over 4
=item Any chord can be changed to a dominant
=item Any dominant chord can be changed to a 9, 11, or 13
=item Any chord can be changed to a chord a tritone away
=back
=head2 sub_cond
The subroutine to determine if a chord substitution should happen.
Default: C<sub { int rand 4 == 0 }> (25% of the time)
=head2 flat
Use flats instead of sharps in the generated chords.
Default: C<0>
=head2 graph
The network transition L<Graph> object. This is a computed attribute.
Default: C<Graph::Directed-E<gt>new>
=head2 phrase
The generated phrase of named chords. This is a computed attribute.
=head2 chords
The generated phrase of individual note chords. This is a computed
attribute.
=head2 verbose
Show the progress and chosen values.
=head1 METHODS
=head2 new
$prog = Music::Chord::Progression->new; # Use the defaults
$prog = Music::Chord::Progression->new( # Override the defaults
max => 4,
net => { 1 => [...], ... 7 => [...] },
chord_map => ['m','dim','','m','m','',''],
scale_name => 'minor',
scale_note => 'A',
octave => 5,
tonic => 0,
resolve => -1,
flat => 1,
substitute => 1,
verbose => 1,
);
Create a new C<Music::Chord::Progression> object.
=head2 generate
$chords = $prog->generate;
Generate a fresh chord progression and set the B<phrase> and B<chords>
attributes.
=head2 substitution
$substitute = $prog->substitution($chord_type);
Perform a jazz substitution on the given the chord type.
=head1 SEE ALSO
The F<t/01-methods.t> test and F<eg/*> example files
L<Carp>
L<Data::Dumper::Compact>
L<Graph>
L<Moo>
L<Music::Chord::Note>
L<Music::Scales>
=head1 AUTHOR
Gene Boggs <gene@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2020-2023 by Gene Boggs.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut