File Coverage

blib/lib/Music/Chord/Progression.pm
Criterion Covered Total %
statement 104 128 81.2
branch 38 72 52.7
condition 12 26 46.1
subroutine 14 15 93.3
pod 2 2 100.0
total 170 243 69.9


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