File Coverage

blib/lib/MIDI/Bassline/Walk.pm
Criterion Covered Total %
statement 138 149 92.6
branch 49 72 68.0
condition 29 85 34.1
subroutine 21 22 95.4
pod 1 1 100.0
total 238 329 72.3


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