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.0706';
7              
8 2     2   704882 use strict;
  2         5  
  2         136  
9 2     2   12 use warnings;
  2         4  
  2         156  
10              
11 2     2   19 use Carp qw(croak);
  2         9  
  2         160  
12 2     2   1197 use File::ShareDir qw(dist_dir);
  2         70432  
  2         184  
13 2     2   20 use List::Util qw(any zip);
  2         3  
  2         144  
14 2     2   1059 use Music::Note ();
  2         3973  
  2         51  
15 2     2   2051 use Text::CSV_XS ();
  2         48442  
  2         67  
16 2     2   1246 use Moo;
  2         17601  
  2         17  
17 2     2   5168 use strictures 2;
  2         4273  
  2         113  
18 2     2   2253 use namespace::clean;
  2         41129  
  2         31  
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   33 my ($self) = @_;
41              
42 3         74 my $file = $self->voicing_file;
43              
44 3         27 my %data;
45              
46 3         48 my $csv = Text::CSV_XS->new({ binary => 1 });
47              
48 3 50       800 open my $fh, '<', $file
49             or croak "Can't read $file: $!";
50              
51 3         289 while (my $row = $csv->getline($fh)) {
52 336         673 my $chord = shift @$row;
53 336         627 my $fingering = shift @$row;
54 336         542 push @{ $data{$chord}{fingering} }, $fingering;
  336         969  
55 336         594 my @notes;
56 336         634 for my $r (@$row) {
57 2016 100       5152 push @notes, $r if $r ne '';
58             }
59 336         551 push @{ $data{$chord}{notes} }, \@notes;
  336         3486  
60             }
61              
62 3         45 close $fh;
63              
64 3         72 return \%data;
65             }
66              
67              
68             sub transform {
69 8     8 1 15513 my ($self, $target, $chord_name, $variation) = @_;
70              
71 8         35 $target = $self->pitchnum($target);
72 8 100       560 croak 'Invalid note' unless $target;
73              
74 7   50     24 $chord_name //= '';
75              
76 7         14 my @notes;
77              
78 7 100       19 if (defined $variation) {
79 6         226 my $pitches = $self->chords->{ 'C' . $chord_name }{notes}[$variation];
80              
81 6         59 my $diff = $target - _lowest_c($pitches);
82              
83 6         16 @notes = map { $_ + $diff } @$pitches;
  18         45  
84             }
85             else {
86 1         3 for my $pitches (@{ $self->chords->{ 'C' . $chord_name }{notes} }) {
  1         34  
87 3         17 my $diff = $target - _lowest_c($pitches);
88 3         8 push @notes, [ map { $_ + $diff } @$pitches ];
  15         35  
89             }
90             }
91              
92 7         27 return \@notes;
93             }
94              
95             sub _lowest_c {
96 27     27   57 my ($pitches) = @_;
97              
98 27         49 my $lowest = 0;
99              
100 27         59 for my $c (48, 60, 72) {
101 37 100   53   205 if (any { $_ == $c } @$pitches) {
  53         150  
102 25         42 $lowest = $c;
103 25         47 last;
104             }
105             }
106              
107 27         94 return $lowest;
108             }
109              
110              
111             sub voicings {
112 3     3 1 10138 my ($self, $chord_name, $format) = @_;
113              
114 3   50     13 $chord_name //= '';
115 3   100     14 $format ||= '';
116              
117 3         112 my $voicings = $self->chords->{ 'C' . $chord_name }{notes};
118              
119 3 100       30 if ($format) {
120 2         4 my $temp;
121              
122 2         6 for my $chord (@$voicings) {
123 6         23 my $span;
124              
125 6         14 for my $n (@$chord) {
126 30         90 my $note = Music::Note->new($n, 'midinum')->format($format);
127 30         1638 push @$span, $note;
128             }
129              
130 6         17 push @$temp, $span;
131             }
132              
133 2         4 $voicings = $temp;
134             }
135              
136 3         14 return $voicings;
137             }
138              
139              
140             sub fingering {
141 10     10 1 14178 my ($self, $target, $chord_name, $variation) = @_;
142              
143 10         44 $target = $self->pitchnum($target);
144              
145 10   50     703 $chord_name //= '';
146              
147 10         22 my @fingering;
148              
149 10 100       25 if (defined $variation) {
150 8         290 my $fingering = $self->chords->{ 'C' . $chord_name }{fingering}[$variation];
151 8         279 my $pitches = $self->chords->{ 'C' . $chord_name }{notes}[$variation];
152              
153 8         97 my ($str, $p) = _find_fingering($target, $pitches, $fingering);
154              
155 8 50       40 push @fingering, $str . '-' . $p if $p >= 0;
156             }
157             else {
158 2         70 for (zip $self->chords->{ 'C' . $chord_name }{notes}, $self->chords->{ 'C' . $chord_name }{fingering}) {
159 10         105 my ($pitches, $fingering) = @$_;
160              
161 10         21 my ($str, $p) = _find_fingering($target, $pitches, $fingering);
162              
163 10 100       38 push @fingering, $str . '-' . $p if $p >= 0;
164             }
165             }
166              
167 10         50 return \@fingering;
168             }
169              
170             # XXX This is overly complicated, questionable logic
171             sub _find_fingering {
172 18     18   45 my ($target, $pitches, $fingering) = @_;
173              
174 18         43 my $diff = $target - _lowest_c($pitches);
175              
176 18         62 my ($str, $pos) = split /-/, $fingering;
177              
178 18         41 my $p = $pos + $diff;
179              
180 18 100 66     104 if ($pos != 1 && $str !~ /0/) {
    100 66        
181 12 100 66     81 if ($p == 0 && $str !~ /0/) {
    50 33        
182 4         11 $str = _decrement_fingering($str);
183 4         8 $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         7 $str = _increment_fingering($str);
191 2         32 $p--;
192             }
193              
194 18         57 return $str, $p;
195             }
196              
197              
198             sub _increment_fingering {
199 2     2   5 my ($fingering) = @_;
200 2         5 my $incremented = '';
201 2         9 for my $char (split //, $fingering) {
202 12 100       42 $incremented .= $char =~ /\d/ ? $char + 1 : $char;
203             }
204 2         7 return $incremented;
205             }
206              
207             sub _decrement_fingering {
208 4     4   9 my ($fingering) = @_;
209 4         9 my $decremented = '';
210 4         15 for my $char (split //, $fingering) {
211 24 100       83 $decremented .= $char =~ /\d/ ? $char - 1 : $char;
212             }
213 4         14 return $decremented;
214             }
215              
216             1;
217              
218             __END__