File Coverage

blib/lib/MIDI/Chord/Guitar.pm
Criterion Covered Total %
statement 120 125 96.0
branch 27 30 90.0
condition 12 20 60.0
subroutine 19 20 95.0
pod 3 3 100.0
total 181 198 91.4


line stmt bran cond sub pod time code
1             package MIDI::Chord::Guitar;
2             our $AUTHORITY = 'cpan:GENE';
3              
4             # ABSTRACT: MIDI pitches for guitar chord voicings
5              
6             our $VERSION = '0.0704';
7              
8 1     1   619 use strict;
  1         2  
  1         22  
9 1     1   4 use warnings;
  1         2  
  1         23  
10              
11 1     1   4 use Carp qw(croak);
  1         2  
  1         42  
12 1     1   343 use File::ShareDir qw(dist_dir);
  1         19789  
  1         48  
13 1     1   7 use List::Util qw(any zip);
  1         2  
  1         78  
14 1     1   336 use Music::Note;
  1         1229  
  1         24  
15 1     1   663 use Text::CSV_XS ();
  1         14248  
  1         24  
16 1     1   407 use Moo;
  1         8411  
  1         5  
17 1     1   1393 use strictures 2;
  1         1216  
  1         30  
18 1     1   491 use namespace::clean;
  1         8594  
  1         5  
19              
20             with('Music::PitchNum');
21              
22              
23             has voicing_file => (
24             is => 'lazy',
25             );
26              
27             sub _build_voicing_file {
28 0     0   0 my ($self) = @_;
29 0         0 my $file = eval { dist_dir('MIDI-Chord-Guitar') . '/midi-guitar-chord-voicings.csv' };
  0         0  
30 0         0 return $file;
31             }
32              
33              
34             has chords => (
35             is => 'lazy',
36             init_arg => undef,
37             );
38              
39             sub _build_chords {
40 3     3   23 my ($self) = @_;
41              
42 3         49 my $file = $self->voicing_file;
43              
44 3         18 my %data;
45              
46 3         23 my $csv = Text::CSV_XS->new({ binary => 1 });
47              
48 3 50       499 open my $fh, '<', $file
49             or croak "Can't read $file: $!";
50              
51 3         175 while (my $row = $csv->getline($fh)) {
52 336         7702 my $chord = shift @$row;
53 336         516 my $fingering = shift @$row;
54 336         308 push @{ $data{$chord}{fingering} }, $fingering;
  336         653  
55 336         357 my @notes;
56 336         480 for my $r (@$row) {
57 2016 100       3491 push @notes, $r if $r ne '';
58             }
59 336         317 push @{ $data{$chord}{notes} }, \@notes;
  336         5308  
60             }
61              
62 3         114 close $fh;
63              
64 3         46 return \%data;
65             }
66              
67              
68             sub transform {
69 8     8 1 9475 my ($self, $target, $chord_name, $variation) = @_;
70              
71 8         134 $target = $self->pitchnum($target);
72 8 100       469 croak 'Invalid note' unless $target;
73              
74 7   50     15 $chord_name //= '';
75              
76 7         8 my @notes;
77              
78 7 100       14 if (defined $variation) {
79 6         102 my $pitches = $self->chords->{ 'C' . $chord_name }{notes}[$variation];
80              
81 6         39 my $diff = $target - _lowest_c($pitches);
82              
83 6         9 @notes = map { $_ + $diff } @$pitches;
  18         26  
84             }
85             else {
86 1         3 for my $pitches (@{ $self->chords->{ 'C' . $chord_name }{notes} }) {
  1         17  
87 3         13 my $diff = $target - _lowest_c($pitches);
88 3         6 push @notes, [ map { $_ + $diff } @$pitches ];
  15         30  
89             }
90             }
91              
92 7         18 return \@notes;
93             }
94              
95             sub _lowest_c {
96 27     27   31 my ($pitches) = @_;
97              
98 27         25 my $lowest = 0;
99              
100 27         35 for my $c (48, 60, 72) {
101 37 100   53   133 if (any { $_ == $c } @$pitches) {
  53         87  
102 25         25 $lowest = $c;
103 25         26 last;
104             }
105             }
106              
107 27         60 return $lowest;
108             }
109              
110              
111             sub voicings {
112 3     3 1 5495 my ($self, $chord_name, $format) = @_;
113              
114 3   50     8 $chord_name //= '';
115 3   100     10 $format ||= '';
116              
117 3         57 my $voicings = $self->chords->{ 'C' . $chord_name }{notes};
118              
119 3 100       18 if ($format) {
120 2         4 my $temp;
121              
122 2         5 for my $chord (@$voicings) {
123 6         7 my $span;
124              
125 6         7 for my $n (@$chord) {
126 30         52 my $note = Music::Note->new($n, 'midinum')->format($format);
127 30         1337 push @$span, $note;
128             }
129              
130 6         10 push @$temp, $span;
131             }
132              
133 2         2 $voicings = $temp;
134             }
135              
136 3         8 return $voicings;
137             }
138              
139              
140             sub fingering {
141 10     10 1 6731 my ($self, $target, $chord_name, $variation) = @_;
142              
143 10         26 $target = $self->pitchnum($target);
144              
145 10   50     405 $chord_name //= '';
146              
147 10         12 my @fingering;
148              
149 10 100       15 if (defined $variation) {
150 8         143 my $fingering = $self->chords->{ 'C' . $chord_name }{fingering}[$variation];
151 8         128 my $pitches = $self->chords->{ 'C' . $chord_name }{notes}[$variation];
152              
153 8         46 my ($str, $p) = _find_fingering($target, $pitches, $fingering);
154              
155 8 50       26 push @fingering, $str . '-' . $p if $p >= 0;
156             }
157             else {
158 2         33 for (zip $self->chords->{ 'C' . $chord_name }{notes}, $self->chords->{ 'C' . $chord_name }{fingering}) {
159 10         39 my ($pitches, $fingering) = @$_;
160              
161 10         14 my ($str, $p) = _find_fingering($target, $pitches, $fingering);
162              
163 10 100       21 push @fingering, $str . '-' . $p if $p >= 0;
164             }
165             }
166              
167 10         25 return \@fingering;
168             }
169              
170             # XXX This is overly complicated, questionable logic
171             sub _find_fingering {
172 18     18   29 my ($target, $pitches, $fingering) = @_;
173              
174 18         25 my $diff = $target - _lowest_c($pitches);
175              
176 18         38 my ($str, $pos) = split /-/, $fingering;
177              
178 18         24 my $p = $pos + $diff;
179              
180 18 100 66     63 if ($pos != 1 && $str !~ /0/) {
    100 66        
181 12 100 66     39 if ($p == 0 && $str !~ /0/) {
    50 33        
182 4         8 $str = _decrement_fingering($str);
183 4         6 $p++;
184             }
185             elsif ($p != 0 && $str =~ /0/) {
186 0         0 $str = _increment_fingering($str);
187             }
188             }
189             elsif ($p > 1 && $str =~ /0/) {
190 2         6 $str = _increment_fingering($str);
191 2         3 $p--;
192             }
193              
194 18         32 return $str, $p;
195             }
196              
197              
198             sub _increment_fingering {
199 2     2   4 my ($fingering) = @_;
200 2         4 my $incremented = '';
201 2         6 for my $char (split //, $fingering) {
202 12 100       26 $incremented .= $char =~ /\d/ ? $char + 1 : $char;
203             }
204 2         5 return $incremented;
205             }
206              
207             sub _decrement_fingering {
208 4     4   7 my ($fingering) = @_;
209 4         4 my $decremented = '';
210 4         9 for my $char (split //, $fingering) {
211 24 100       50 $decremented .= $char =~ /\d/ ? $char - 1 : $char;
212             }
213 4         17 return $decremented;
214             }
215              
216             1;
217              
218             __END__