File Coverage

blib/lib/MIDI/Bassline/Walk.pm
Criterion Covered Total %
statement 139 150 92.6
branch 49 72 68.0
condition 29 85 34.1
subroutine 21 22 95.4
pod 1 1 100.0
total 239 330 72.4


line stmt bran cond sub pod time code
1             package MIDI::Bassline::Walk;
2             our $AUTHORITY = 'cpan:GENE';
3              
4             # ABSTRACT: Generate walking basslines
5              
6             our $VERSION = '0.0507';
7              
8 1     1   1261 use Moo;
  1         11398  
  1         5  
9 1     1   1891 use strictures 2;
  1         1711  
  1         40  
10 1     1   751 use Data::Dumper::Compact qw(ddc);
  1         13808  
  1         5  
11 1     1   137 use Carp qw(croak);
  1         3  
  1         70  
12 1     1   547 use List::SomeUtils qw(first_index);
  1         10100  
  1         82  
13 1     1   10 use List::Util qw(any min uniq);
  1         2  
  1         55  
14 1     1   471 use Music::Chord::Note ();
  1         1162  
  1         25  
15 1     1   481 use Music::Note ();
  1         1645  
  1         31  
16 1     1   432 use Music::Scales qw(get_scale_notes get_scale_MIDI);
  1         5110  
  1         78  
17 1     1   478 use Music::VoiceGen ();
  1         39329  
  1         29  
18 1     1   595 use Set::Array ();
  1         9648  
  1         39  
19 1     1   13 use namespace::clean;
  1         3  
  1         10  
20              
21 1     1   485 use constant E1 => 28; # lowest note on a bass guitar in standard tuning
  1         10  
  1         2398  
22              
23             with('Music::PitchNum');
24              
25              
26             has guitar => (
27             is => 'ro',
28             isa => \&_boolean,
29             default => sub { 0 },
30             );
31              
32              
33             has wrap => (
34             is => 'ro',
35             isa => sub { croak 'not valid' unless $_[0] =~ /^[0A-G][#b]?\d?$/ },
36             default => sub { 0 },
37             );
38              
39              
40             has modal => (
41             is => 'ro',
42             isa => \&_boolean,
43             default => sub { 0 },
44             );
45              
46              
47             has chord_notes => (
48             is => 'ro',
49             isa => \&_boolean,
50             default => sub { 1 },
51             );
52              
53              
54             has keycenter => (
55             is => 'ro',
56             isa => sub { croak 'not a valid pitch' unless $_[0] =~ /^[A-G][#b]?$/ },
57             default => sub { 'C' },
58             );
59              
60              
61             has intervals => (
62             is => 'ro',
63             isa => sub { croak 'not an array reference' unless ref $_[0] eq 'ARRAY' },
64             default => sub { [qw(-3 -2 -1 1 2 3)] },
65             );
66              
67              
68             has octave => (
69             is => 'ro',
70             isa => sub { croak 'not a positive integer' unless $_[0] =~ /^\d+$/ },
71             default => sub { 1 },
72             );
73              
74              
75             has scale => (
76             is => 'ro',
77             isa => sub { croak 'not a code reference' unless ref $_[0] eq 'CODE' },
78             builder => 1,
79             );
80              
81             sub _build_scale {
82 11     11   89 my ($self) = @_;
83 11 100       38 if ($self->modal) {
84             return sub {
85 5     5   1038 my ($chord) = @_;
86 5         14 my ($chord_note) = _parse_chord($chord);
87 5         18 my @modes = qw( ionian dorian phrygian lydian mixolydian aeolian locrian );
88 5         24 my @key_notes = get_scale_notes($self->keycenter, $modes[0]);
89 5         958 my $position = first_index { $_ eq $chord_note } @key_notes;
  8         19  
90 5 50       27 my $scale = $position >= 0 ? $modes[$position] : $modes[0];
91 5         17 return $scale;
92 4         79 };
93             }
94             else {
95 7 100   8   125 return sub { $_[0] =~ /^[A-G][#b]?m/ ? 'minor' : 'major' };
  8         1613  
96             }
97             }
98              
99              
100             has tonic => (
101             is => 'ro',
102             isa => \&_boolean,
103             default => sub { 0 },
104             );
105              
106              
107             has verbose => (
108             is => 'ro',
109             isa => \&_boolean,
110             default => sub { 0 },
111             );
112              
113             sub _boolean {
114 70     70   2320 my ($arg) = @_;
115 70 100       1162 croak 'not a boolean' unless $arg =~ /^[01]$/;
116             }
117              
118              
119             sub generate {
120 7     7 1 3494 my ($self, $chord, $num, $next_chord) = @_;
121              
122 7   50     22 $chord ||= 'C';
123 7   50     15 $num ||= 4;
124              
125 7         18 my ($chord_note, $flavor) = _parse_chord($chord);
126 7 100       24 if ($flavor =~ /^\//) {
127 1         3 $flavor = '';
128 1         2 $chord = $chord_note;
129             }
130              
131 7         11 my $next_chord_note;
132 7 100       16 ($next_chord_note) = _parse_chord($next_chord)
133             if $next_chord;
134              
135 7 50       24 print "CHORD: $chord => $chord_note, $flavor\n" if $self->verbose;
136 7 50 33     17 print "NEXT: $next_chord => $next_chord_note\n" if $self->verbose && $next_chord;
137              
138 7         17 my $scale = $self->scale->($chord);
139 7 100       24 my $next_scale = defined $next_chord ? $self->scale->($next_chord) : '';
140              
141 7         35 my $cn = Music::Chord::Note->new;
142              
143 7         55 my @notes = map { $self->pitchnum($_) }
  24         1634  
144             $cn->chord_with_octave($chord, $self->octave);
145              
146 7 50       330 my @pitches = $scale ? get_scale_MIDI($chord_note, $self->octave, $scale) : ();
147 7 100       368 my @next_pitches = $next_scale ? get_scale_MIDI($next_chord_note, $self->octave, $next_scale) : ();
148              
149             # Add unique chord notes to the pitches
150 7 100       65 if ($self->chord_notes) {
151 6 50       15 print "CHORD NOTES\n" if $self->verbose;
152 6         14 for my $n (@notes) {
153 20 100   71   74 if (not any { $_ == $n } @pitches) {
  71         125  
154 2         4 push @pitches, $n;
155 2 50       7 if ($self->verbose) {
156 0         0 my $x = $self->pitchname($n);
157 0         0 print "\tADD: $x\n";
158             }
159             }
160             }
161             }
162 7         30 @pitches = sort { $a <=> $b } @pitches; # Pitches are midi numbers
  85         118  
163              
164             # Determine if we should skip certain notes given the chord flavor
165 7         20 my @tones = get_scale_notes($chord_note, $scale);
166 7 50       1190 print "\t$scale SCALE: ", ddc(\@tones) if $self->verbose;
167 7         13 my @fixed;
168 7         13 for my $p (@pitches) {
169 51         141 my $n = Music::Note->new($p, 'midinum');
170 51         1269 my $x = $n->format('isobase');
171             # Inspect both # & b
172 51 100       1006 if ($x =~ /#/) {
    50          
173 6         17 $n->en_eq('flat');
174             }
175             elsif ($x =~ /b/) {
176 0         0 $n->en_eq('sharp');
177             }
178 51         171 my $y = $n->format('isobase');
179 51 0 66     1358 if (($flavor =~ /[#b]5/ && $tones[4] && ($x eq $tones[4] || $y eq $tones[4]))
      66        
      33        
      100        
      66        
      66        
      33        
      66        
      33        
      0        
      0        
      66        
      33        
      0        
      0        
      33        
      33        
      0        
      0        
      33        
      33        
      0        
      0        
      33        
180             ||
181             ($flavor =~ /7/ && $flavor !~ /[Mm]7/ && $tones[6] && ($x eq $tones[6] || $y eq $tones[6]))
182             ||
183             ($flavor =~ /[#b]9/ && $tones[1] && ($x eq $tones[1] || $y eq $tones[1]))
184             ||
185             ($flavor =~ /dim/ && $tones[2] && ($x eq $tones[2] || $y eq $tones[2]))
186             ||
187             ($flavor =~ /dim/ && $tones[6] && ($x eq $tones[6] || $y eq $tones[6]))
188             ||
189             ($flavor =~ /aug/ && $tones[6] && ($x eq $tones[6] || $y eq $tones[6]))
190             ) {
191 3 50       13 print "\tDROP: $x\n" if $self->verbose;
192 3         8 next;
193             }
194 48         141 push @fixed, $p;
195             }
196              
197 7 50       23 if ($self->guitar) {
198 0 0       0 @fixed = sort { $a <=> $b } map { $_ < E1 ? $_ + 12 : $_ } @fixed;
  0         0  
  0         0  
199             }
200              
201 7 100       22 if ($self->wrap) {
202 1         52 my $n = Music::Note->new($self->wrap, 'ISO');
203 1         46 $n = $n->format('midinum');
204 1 100       33 @fixed = sort { $a <=> $b } map { $_ > $n ? $_ - 12 : $_ } @fixed;
  14         21  
  7         44  
205             }
206              
207             # Make sure there are no duplicate pitches
208 7         76 @fixed = uniq @fixed;
209 7 50       28 $self->_verbose_notes('NOTES', @fixed) if $self->verbose;
210              
211 7         160 my $voice = Music::VoiceGen->new(
212             pitches => \@fixed,
213             intervals => $self->intervals,
214             );
215              
216             # Try to start the phrase in the middle of the scale
217 7         9621 $voice->context($fixed[int @fixed / 2]);
218              
219             # Get a passage of quasi-random pitches
220 7         626 my @chosen = map { $voice->rand } 1 .. $num;
  218         26695  
221              
222             # Choose the right note given the scale if the tonic is set
223 7 100       933 if ($self->tonic) {
224 1 50 33     12 if ($scale eq 'pentatonic' || $scale eq 'pminor') {
    50          
225 0         0 $chosen[0] = _closest($chosen[1], [ @fixed[0,1,2] ])
226             }
227             elsif (@fixed == 7) { # standard, 7-note Western scale
228 1         6 $chosen[0] = _closest($chosen[1], [ @fixed[0,2,4] ])
229             }
230             }
231              
232             # Intersect with the next-chord pitches
233 7 100       29 if ($next_chord) {
234 1         11 my $A1 = Set::Array->new(@fixed);
235 1         13 my $A2 = Set::Array->new(@next_pitches);
236 1         8 my @intersect = @{ $A1->intersection($A2) };
  1         34  
237 1 50       327 $self->_verbose_notes('INTERSECT', @intersect) if $self->verbose;
238             # Anticipate the next chord
239 1 50       4 if (@intersect) {
240 1 50 33     7 if (my $closest = _closest($chosen[-2] || $chosen[-1], \@intersect)) {
241 1         11 $chosen[-1] = $closest;
242             }
243             }
244             }
245              
246             # Show them what they've won, Bob!
247 7 50       25 $self->_verbose_notes('CHOSEN', @chosen) if $self->verbose;
248              
249 7         150 return \@chosen;
250             }
251              
252             sub _parse_chord {
253 13     13   25 my ($chord) = @_;
254 13         23 my $chord_note;
255             my $flavor;
256 13 50       68 if ($chord =~ /^([A-G][#b]?)(.*)$/) {
257 13         34 $chord_note = $1;
258 13         21 $flavor = $2;
259             }
260 13         35 return $chord_note, $flavor;
261             }
262              
263             # Show a phrase of midinums as ISO notes
264             sub _verbose_notes {
265 0     0   0 my ($self, $title, @notes) = @_;
266 0         0 @notes = map { $self->pitchname($_) } @notes;
  0         0  
267 0         0 print "\t$title: ", ddc(\@notes);
268             }
269              
270             # Find the closest absolute difference to the key, in the list
271             sub _closest {
272 2     2   5 my ($key, $list) = @_;
273             # Remove the key from the list
274 2         5 $list = [ grep { $_ != $key } @$list ];
  8         19  
275 2 50       7 return undef unless @$list;
276             # Find the absolute difference
277 2         5 my @diff = map { abs($key - $_) } @$list;
  7         16  
278 2         8 my $min = min @diff;
279 2         4 my @closest;
280             # Get all the minimum elements of list
281 2         6 for my $n (0 .. $#diff) {
282 7 100       17 next if $diff[$n] != $min;
283 2         5 push @closest, $list->[$n];
284             }
285             # Return a random minimum
286 2         21 return $closest[int rand @closest];
287             }
288              
289             1;
290              
291             __END__