File Coverage

blib/lib/Music/Chord/Progression.pm
Criterion Covered Total %
statement 107 131 81.6
branch 39 72 54.1
condition 12 26 46.1
subroutine 15 16 93.7
pod 2 2 100.0
total 175 247 70.8


line stmt bran cond sub pod time code
1             package Music::Chord::Progression;
2             our $AUTHORITY = 'cpan:GENE';
3              
4             # ABSTRACT: Create network transition chord progressions
5              
6             our $VERSION = '0.0608';
7              
8 1     1   1255 use Moo;
  1         11691  
  1         7  
9 1     1   2045 use strictures 2;
  1         1924  
  1         45  
10              
11 1     1   223 use Carp qw(croak);
  1         2  
  1         44  
12 1     1   582 use Data::Dumper::Compact qw(ddc);
  1         13803  
  1         4  
13 1     1   556 use Graph::Directed ();
  1         34638  
  1         20  
14 1     1   1051 use Music::Chord::Note ();
  1         2427  
  1         36  
15 1     1   555 use Music::Scales qw(get_scale_notes);
  1         5573  
  1         83  
16              
17 1     1   473 use namespace::clean;
  1         12659  
  1         6  
18              
19              
20             has max => (
21             is => 'ro',
22             isa => sub { croak "$_[0] is not a valid integer" unless $_[0] =~ /^\d+$/ },
23             default => sub { 8 },
24             );
25              
26              
27             has net => (
28             is => 'ro',
29             isa => sub { croak "$_[0] is not a hashref" unless ref $_[0] eq 'HASH' },
30             default => sub {
31             { 1 => [qw( 1 2 3 4 5 6 )],
32             2 => [qw( 3 4 5 )],
33             3 => [qw( 1 2 4 6 )],
34             4 => [qw( 1 3 5 6 )],
35             5 => [qw( 1 4 6 )],
36             6 => [qw( 1 2 4 5 )],
37             7 => [] }
38             },
39             );
40              
41              
42             has chord_map => (
43             is => 'lazy',
44             );
45             sub _build_chord_map {
46 5     5   52 my ($self) = @_;
47 5         78 my %modes = (
48             major => [ '', 'm', 'm', '', '', 'm', 'dim' ],
49             ionian => [ '', 'm', 'm', '', '', 'm', 'dim' ],
50             dorian => [ 'm', 'm', '', '', 'm', 'dim', '' ],
51             phrygian => [ 'm', '', '', 'm', 'dim', '', 'm' ],
52             lydian => [ '', '', 'm', 'dim', '', 'm', 'm' ],
53             mixolydian => [ '', 'm', 'dim', '', 'm', 'm', '' ],
54             minor => [ 'm', 'dim', '', 'm', 'm', '', '' ],
55             aeolian => [ 'm', 'dim', '', 'm', 'm', '', '' ],
56             locrian => [ 'dim', '', 'm', 'm', '', '', 'm' ],
57             );
58 5         38 return $modes{ $self->scale_name };
59             }
60              
61              
62             has scale_name => (
63             is => 'ro',
64             isa => sub { croak "$_[0] is not a valid string" if ref $_[0] },
65             default => sub { 'major' },
66             );
67              
68              
69             has scale_note => (
70             is => 'ro',
71             isa => sub { croak "$_[0] is not a valid note" unless $_[0] =~ /^[A-G][#b]?$/ },
72             default => sub { 'C' },
73             );
74              
75              
76             has scale => (
77             is => 'lazy',
78             init_args => undef,
79             );
80              
81             sub _build_scale {
82 6     6   46 my ($self) = @_;
83 6         26 my @scale = get_scale_notes($self->scale_note, $self->scale_name);
84 6         1126 my %equiv = (
85             'C#' => 'Db',
86             'D#' => 'Eb',
87             'E#' => 'F',
88             'F#' => 'Gb',
89             'G#' => 'Ab',
90             'A#' => 'Bb',
91             'B#' => 'C',
92             'Cb' => 'B',
93             'Dbb' => 'C',
94             'Ebb' => 'D',
95             'Fb' => 'E',
96             'Gbb' => 'F',
97             'Abb' => 'G',
98             'Bbb' => 'A',
99             );
100 6         11 for (@scale) {
101 42 50       82 $_ = $equiv{$_} if exists $equiv{$_};
102             }
103 6 50       17 print ucfirst($self->scale_name), ' scale: ', ddc(\@scale) if $self->verbose;
104 6         30 return \@scale;
105             }
106              
107              
108             has octave => (
109             is => 'ro',
110             isa => sub { croak "$_[0] is not a valid octave" unless $_[0] =~ /^-?\d+$/ },
111             default => sub { 4 },
112             );
113              
114              
115             has tonic => (
116             is => 'ro',
117             isa => sub { croak "$_[0] is not a valid setting" unless $_[0] =~ /^-?[01]$/ },
118             default => sub { 1 },
119             );
120              
121              
122             has resolve => (
123             is => 'ro',
124             isa => sub { croak "$_[0] is not a valid setting" unless $_[0] =~ /^-?[01]$/ },
125             default => sub { 1 },
126             );
127              
128              
129             has substitute => (
130             is => 'ro',
131             isa => sub { croak "$_[0] is not a valid boolean" unless $_[0] =~ /^[01]$/ },
132             default => sub { 0 },
133             );
134              
135              
136             has sub_cond => (
137             is => 'ro',
138             isa => sub { croak "$_[0] is not a valid coderef" unless ref($_[0]) eq 'CODE' },
139             default => sub { return sub { int rand 4 == 0 } },
140             );
141              
142              
143             has flat => (
144             is => 'ro',
145             isa => sub { croak "$_[0] is not a valid boolean" unless $_[0] =~ /^[01]$/ },
146             default => sub { 0 },
147             );
148              
149              
150             has graph => (
151             is => 'lazy',
152             init_args => undef,
153             );
154              
155             sub _build_graph {
156 6     6   51 my ($self) = @_;
157 6         51 my $g = Graph::Directed->new;
158 6         4564 for my $posn (keys %{ $self->net }) {
  6         27  
159 42         3324 for my $p (@{ $self->net->{$posn} }) {
  42         100  
160 127         8879 $g->add_edge($posn, $p);
161             }
162             }
163 6         459 return $g;
164             }
165              
166              
167             has phrase => (
168             is => 'rw',
169             init_args => undef,
170             );
171              
172              
173             has chords => (
174             is => 'rw',
175             init_args => undef,
176             );
177              
178              
179              
180             has verbose => (
181             is => 'ro',
182             isa => sub { croak "$_[0] is not a valid boolean" unless $_[0] =~ /^[01]$/ },
183             default => sub { 0 },
184             );
185              
186              
187             sub generate {
188 7     7 1 3313 my ($self) = @_;
189              
190             croak 'chord_map length must equal number of net keys'
191 7 100       11 unless @{ $self->chord_map } == keys %{ $self->net };
  7         155  
  7         65  
192              
193 6 50       19 print 'Graph: ' . $self->graph, "\n" if $self->verbose;
194              
195             # Create a random progression
196 6         9 my @progression;
197             my $v; # Vertex
198 6         19 for my $n (1 .. $self->max) {
199 42         83 $v = $self->_next_successor($n, $v);
200 42         99 push @progression, $v;
201             }
202 6 50       21 print 'Progression: ', ddc(\@progression) if $self->verbose;
203              
204 6         9 my @chord_map = @{ $self->chord_map };
  6         116  
205              
206 6 100       76 if ($self->substitute) {
207 1         5 my $i = 0;
208 1         3 for my $chord (@chord_map) {
209 7 50       23 my $substitute = $self->sub_cond->() ? $self->substitution($chord) : $chord;
210 7 0 33     23 if ($substitute eq $chord && $i < @progression && $self->sub_cond->()) {
      33        
211 0         0 $progression[$i] .= 't'; # Indicate that we should tritone substitute
212             }
213 7         11 $chord = $substitute;
214 7         11 $i++;
215             }
216             }
217 6 50       17 print 'Chord map: ', ddc(\@chord_map) if $self->verbose;
218              
219 6         16 my @phrase = map { $self->_tt_sub(\@chord_map, $_) } @progression;
  42         102  
220 6         31 $self->phrase(\@phrase);
221 6 50       16 print 'Phrase: ', ddc($self->phrase) if $self->verbose;
222              
223             # Add octaves to the chords
224 6         32 my $mcn = Music::Chord::Note->new;
225 6         32 my @chords;
226 6         12 for my $chord (@phrase) {
227 42         104 my @chord = $mcn->chord_with_octave($chord, $self->octave);
228 42         3660 push @chords, \@chord;
229             }
230              
231 6 100       17 if ($self->flat) {
232 1         19 my %equiv = (
233             'C#' => 'Db',
234             'D#' => 'Eb',
235             'E#' => 'F',
236             'F#' => 'Gb',
237             'G#' => 'Ab',
238             'A#' => 'Bb',
239             'B#' => 'C',
240             );
241 1         3 for my $chord (@chords) {
242 8         16 for my $note (@$chord) {
243 24 100       88 $note =~ s/^([A-G]#)(\d+)$/$equiv{$1}$2/ if $note =~ /#/;
244             }
245             }
246             }
247              
248 6         27 $self->chords(\@chords);
249 6 50       29 print 'Chords: ', ddc($self->chords) if $self->verbose;
250              
251 6         43 return \@chords;
252             }
253              
254             sub _next_successor {
255 42     42   74 my ($self, $n, $v) = @_;
256              
257 42   100     105 $v //= 1;
258              
259 42         55 my $s;
260              
261 42 100       117 if ($n == 1) {
    100          
262 6 50       18 if ($self->tonic == 0) {
    50          
263 0         0 $s = $self->graph->random_successor(1);
264             }
265             elsif ($self->tonic == 1) {
266 6         10 $s = 1;
267             }
268             else {
269 0         0 $s = $self->_full_keys;
270             }
271             }
272             elsif ($n == $self->max) {
273 6 100       34 if ($self->resolve == 0) {
    50          
274 1   33     19 $s = $self->graph->random_successor($v) || $self->_full_keys;
275             }
276             elsif ($self->resolve == 1) {
277 5         14 $s = 1;
278             }
279             else {
280 0         0 $s = $self->_full_keys;
281             }
282             }
283             else {
284 30         663 $s = $self->graph->random_successor($v);
285             }
286              
287 42         11477 return $s;
288             }
289              
290             sub _full_keys {
291 0     0   0 my ($self) = @_;
292 0         0 my @keys = grep { keys @{ $self->net->{$_} } > 0 } keys %{ $self->net };
  0         0  
  0         0  
  0         0  
293 0         0 return $keys[int rand @keys];
294             }
295              
296             sub _tt_sub {
297 42     42   79 my ($self, $chord_map, $n) = @_;
298              
299 42         55 my $note;
300              
301 42 50       91 if ($n =~ /t/) {
302 0         0 my @fnotes = get_scale_notes('C', 'chromatic', 0, 'b');
303 0         0 my @snotes = get_scale_notes('C', 'chromatic');
304 0         0 my %ftritone = map { $fnotes[$_] => $fnotes[($_ + 6) % @fnotes] } 0 .. $#fnotes;
  0         0  
305 0         0 my %stritone = map { $snotes[$_] => $snotes[($_ + 6) % @snotes] } 0 .. $#snotes;
  0         0  
306              
307 0         0 $n =~ s/t//;
308 0   0     0 $note = $ftritone{ $self->scale->[$n - 1] } || $stritone{ $self->scale->[$n - 1] };
309 0 0       0 print 'Tritone: ', $self->scale->[$n - 1], " => $note\n" if $self->verbose;
310             }
311             else {
312 42         774 $note = $self->scale->[$n - 1];
313             }
314              
315 42         276 $note .= $chord_map->[$n - 1];
316 42 50       92 print "Note: $note\n" if $self->verbose;
317              
318 42         105 return $note;
319             }
320              
321              
322             sub substitution {
323 10     10 1 1173 my ($self, $chord) = @_;
324              
325 10         16 my $substitute = $chord;
326              
327 10 100 100     51 if ($chord eq '' || $chord eq 'm') {
    100 66        
    50 33        
    50          
    50          
    0          
328 8         17 my $roll = int rand 2;
329 8 100       20 $substitute = $roll == 0 ? $chord . 'M7' : $chord . 7;
330             }
331             elsif ($chord eq 'dim' || $chord eq 'aug') {
332 1         16 $substitute = $chord . 7;
333             }
334             elsif ($chord eq '-5' || $chord eq '-9') {
335 0         0 $substitute = "7($chord)";
336             }
337             elsif ($chord eq 'M7') {
338 0         0 my $roll = int rand 3;
339 0 0       0 $substitute = $roll == 0 ? 'M9' : $roll == 1 ? 'M11' : 'M13';
    0          
340             }
341             elsif ($chord eq '7') {
342 1         4 my $roll = int rand 3;
343 1 50       4 $substitute = $roll == 0 ? '9' : $roll == 1 ? '11' : '13';
    50          
344             }
345             elsif ($chord eq 'm7') {
346 0         0 my $roll = int rand 3;
347 0 0       0 $substitute = $roll == 0 ? 'm9' : $roll == 1 ? 'm11' : 'm13';
    0          
348             }
349              
350 10 50 33     28 print qq|Substitute: "$chord" => "$substitute"\n| if $self->verbose && $substitute ne $chord;
351              
352 10         22 return $substitute;
353             }
354              
355             1;
356              
357             __END__