File Coverage

blib/lib/Music/Chord/Progression.pm
Criterion Covered Total %
statement 107 131 81.6
branch 38 72 52.7
condition 12 26 46.1
subroutine 15 16 93.7
pod 2 2 100.0
total 174 247 70.4


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