File Coverage

blib/lib/MIDI/Bassline/Walk.pm
Criterion Covered Total %
statement 128 147 87.0
branch 40 70 57.1
condition 28 85 32.9
subroutine 21 22 95.4
pod 1 1 100.0
total 218 325 67.0


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.0506';
7              
8 1     1   1187 use Moo;
  1         11152  
  1         4  
9 1     1   1870 use strictures 2;
  1         1663  
  1         37  
10 1     1   714 use Data::Dumper::Compact qw(ddc);
  1         13352  
  1         9  
11 1     1   135 use Carp qw(croak);
  1         2  
  1         60  
12 1     1   556 use List::SomeUtils qw(first_index);
  1         10059  
  1         72  
13 1     1   7 use List::Util qw(any min uniq);
  1         2  
  1         51  
14 1     1   456 use Music::Chord::Note ();
  1         1103  
  1         25  
15 1     1   472 use Music::Note ();
  1         1666  
  1         30  
16 1     1   446 use Music::Scales qw(get_scale_notes get_scale_MIDI);
  1         5102  
  1         69  
17 1     1   462 use Music::VoiceGen ();
  1         38904  
  1         28  
18 1     1   577 use Set::Array ();
  1         9231  
  1         25  
19 1     1   7 use namespace::clean;
  1         4  
  1         8  
20              
21 1     1   406 use constant E1 => 28; # lowest note on a bass guitar in standard tuning
  1         7  
  1         2290  
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 10     10   82 my ($self) = @_;
83 10 100       36 if ($self->modal) {
84             return sub {
85 4     4   422 my ($chord) = @_;
86 4         10 my ($chord_note) = _parse_chord($chord);
87 4         16 my @modes = qw( ionian dorian phrygian lydian mixolydian aeolian locrian );
88 4         21 my @key_notes = get_scale_notes($self->keycenter, $modes[0]);
89 4         792 my $position = first_index { $_ eq $chord_note } @key_notes;
  7         17  
90 4 50       22 my $scale = $position >= 0 ? $modes[$position] : $modes[0];
91 4         13 return $scale;
92 4         81 };
93             }
94             else {
95 6 100   4   112 return sub { $_[0] =~ /^[A-G][#b]?m/ ? 'minor' : 'major' };
  4         3056  
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 65     65   2437 my ($arg) = @_;
115 65 100       1134 croak 'not a boolean' unless $arg =~ /^[01]$/;
116             }
117              
118              
119             sub generate {
120 5     5 1 2175 my ($self, $chord, $num, $next_chord) = @_;
121              
122 5   50     17 $chord ||= 'C';
123 5   50     12 $num ||= 4;
124              
125 5         11 my ($chord_note, $flavor) = _parse_chord($chord);
126              
127 5         9 my $next_chord_note;
128 5 50       14 ($next_chord_note) = _parse_chord($next_chord)
129             if $next_chord;
130              
131 5 50       18 print "CHORD: $chord => $chord_note, $flavor\n" if $self->verbose;
132 5 50 33     20 print "NEXT: $next_chord => $next_chord_note\n" if $self->verbose && $next_chord;
133              
134 5         21 my $scale = $self->scale->($chord);
135 5 50       13 my $next_scale = defined $next_chord ? $self->scale->($next_chord) : '';
136              
137 5         29 my $cn = Music::Chord::Note->new;
138              
139 5         43 my @notes = map { $self->pitchnum($_) }
  18         1344  
140             $cn->chord_with_octave($chord, $self->octave);
141              
142 5 50       240 my @pitches = $scale ? get_scale_MIDI($chord_note, $self->octave, $scale) : ();
143 5 50       292 my @next_pitches = $next_scale ? get_scale_MIDI($next_chord_note, $self->octave, $next_scale) : ();
144              
145             # Add unique chord notes to the pitches
146 5 100       19 if ($self->chord_notes) {
147 4 50       10 print "CHORD NOTES\n" if $self->verbose;
148 4         11 for my $n (@notes) {
149 14 100   53   51 if (not any { $_ == $n } @pitches) {
  53         95  
150 2         5 push @pitches, $n;
151 2 50       10 if ($self->verbose) {
152 0         0 my $x = $self->pitchname($n);
153 0         0 print "\tADD: $x\n";
154             }
155             }
156             }
157             }
158 5         24 @pitches = sort { $a <=> $b } @pitches; # Pitches are midi numbers
  63         97  
159              
160             # Determine if we should skip certain notes given the chord flavor
161 5         14 my @tones = get_scale_notes($chord_note, $scale);
162 5 50       903 print "\t$scale SCALE: ", ddc(\@tones) if $self->verbose;
163 5         7 my @fixed;
164 5         12 for my $p (@pitches) {
165 37         98 my $n = Music::Note->new($p, 'midinum');
166 37         923 my $x = $n->format('isobase');
167             # Inspect both # & b
168 37 100       749 if ($x =~ /#/) {
    50          
169 2         8 $n->en_eq('flat');
170             }
171             elsif ($x =~ /b/) {
172 0         0 $n->en_eq('sharp');
173             }
174 37         110 my $y = $n->format('isobase');
175 37 0 66     1025 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        
176             ||
177             ($flavor =~ /7/ && $flavor !~ /[Mm]7/ && $tones[6] && ($x eq $tones[6] || $y eq $tones[6]))
178             ||
179             ($flavor =~ /[#b]9/ && $tones[1] && ($x eq $tones[1] || $y eq $tones[1]))
180             ||
181             ($flavor =~ /dim/ && $tones[2] && ($x eq $tones[2] || $y eq $tones[2]))
182             ||
183             ($flavor =~ /dim/ && $tones[6] && ($x eq $tones[6] || $y eq $tones[6]))
184             ||
185             ($flavor =~ /aug/ && $tones[6] && ($x eq $tones[6] || $y eq $tones[6]))
186             ) {
187 3 50       12 print "\tDROP: $x\n" if $self->verbose;
188 3         8 next;
189             }
190 34         108 push @fixed, $p;
191             }
192              
193 5 50       18 if ($self->guitar) {
194 0 0       0 @fixed = sort { $a <=> $b } map { $_ < E1 ? $_ + 12 : $_ } @fixed;
  0         0  
  0         0  
195             }
196              
197 5 100       15 if ($self->wrap) {
198 1         7 my $n = Music::Note->new($self->wrap, 'ISO');
199 1         41 $n = $n->format('midinum');
200 1 100       30 @fixed = sort { $a <=> $b } map { $_ > $n ? $_ - 12 : $_ } @fixed;
  14         22  
  7         18  
201             }
202              
203             # Make sure there are no duplicate pitches
204 5         69 @fixed = uniq @fixed;
205 5 50       24 $self->_verbose_notes('NOTES', @fixed) if $self->verbose;
206              
207 5         107 my $voice = Music::VoiceGen->new(
208             pitches => \@fixed,
209             intervals => $self->intervals,
210             );
211              
212             # Try to start the phrase in the middle of the scale
213 5         7943 $voice->context($fixed[int @fixed / 2]);
214              
215             # Get a passage of quasi-random pitches
216 5         443 my @chosen = map { $voice->rand } 1 .. $num;
  210         26332  
217              
218             # Choose the right note given the scale if the tonic is set
219 5 100       646 if ($self->tonic) {
220 1 50 33     11 if ($scale eq 'pentatonic' || $scale eq 'pminor') {
    50          
221 0         0 $chosen[0] = _closest($chosen[1], [ @fixed[0,1,2] ])
222             }
223             elsif (@fixed == 7) { # standard, 7-note Western scale
224 1         7 $chosen[0] = _closest($chosen[1], [ @fixed[0,2,4] ])
225             }
226             }
227              
228             # Intersect with the next-chord pitches
229 5 50       16 if ($next_chord) {
230 0         0 my $A1 = Set::Array->new(@fixed);
231 0         0 my $A2 = Set::Array->new(@next_pitches);
232 0         0 my @intersect = @{ $A1->intersection($A2) };
  0         0  
233 0 0       0 $self->_verbose_notes('INTERSECT', @intersect) if $self->verbose;
234             # Anticipate the next chord
235 0 0       0 if (@intersect) {
236 0 0 0     0 if (my $closest = _closest($chosen[-2] || $chosen[-1], \@intersect)) {
237 0         0 $chosen[-1] = $closest;
238             }
239             }
240             }
241              
242             # Show them what they've won, Bob!
243 5 50       14 $self->_verbose_notes('CHOSEN', @chosen) if $self->verbose;
244              
245 5         104 return \@chosen;
246             }
247              
248             sub _parse_chord {
249 9     9   17 my ($chord) = @_;
250 9         17 my $chord_note;
251             my $flavor;
252 9 50       51 if ($chord =~ /^([A-G][#b]?)(.*)$/) {
253 9         25 $chord_note = $1;
254 9         17 $flavor = $2;
255             }
256 9         26 return $chord_note, $flavor;
257             }
258              
259             # Show a phrase of midinums as ISO notes
260             sub _verbose_notes {
261 0     0   0 my ($self, $title, @notes) = @_;
262 0         0 @notes = map { $self->pitchname($_) } @notes;
  0         0  
263 0         0 print "\t$title: ", ddc(\@notes);
264             }
265              
266             # Find the closest absolute difference to the key, in the list
267             sub _closest {
268 1     1   3 my ($key, $list) = @_;
269             # Remove the key from the list
270 1         3 $list = [ grep { $_ != $key } @$list ];
  3         9  
271 1 50       4 return undef unless @$list;
272             # Find the absolute difference
273 1         2 my @diff = map { abs($key - $_) } @$list;
  2         6  
274 1         6 my $min = min @diff;
275 1         2 my @closest;
276             # Get all the minimum elements of list
277 1         4 for my $n (0 .. $#diff) {
278 2 100       6 next if $diff[$n] != $min;
279 1         3 push @closest, $list->[$n];
280             }
281             # Return a random minimum
282 1         6 return $closest[int rand @closest];
283             }
284              
285             1;
286              
287             __END__