File Coverage

blib/lib/Music/Bassline/Generator.pm
Criterion Covered Total %
statement 153 165 92.7
branch 53 74 71.6
condition 30 85 35.2
subroutine 24 26 92.3
pod 1 1 100.0
total 261 351 74.3


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