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.0705';
7              
8 1     1   587 use strict;
  1         1  
  1         25  
9 1     1   4 use warnings;
  1         1  
  1         23  
10              
11 1     1   4 use Carp qw(croak);
  1         1  
  1         41  
12 1     1   559 use File::ShareDir qw(dist_dir);
  1         24697  
  1         48  
13 1     1   7 use List::Util qw(any zip);
  1         1  
  1         83  
14 1     1   354 use Music::Note ();
  1         1582  
  1         20  
15 1     1   816 use Text::CSV_XS ();
  1         18075  
  1         27  
16 1     1   512 use Moo;
  1         10094  
  1         4  
17 1     1   1778 use strictures 2;
  1         1375  
  1         34  
18 1     1   587 use namespace::clean;
  1         9662  
  1         6  
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   24 my ($self) = @_;
41              
42 3         45 my $file = $self->voicing_file;
43              
44 3         22 my %data;
45              
46 3         24 my $csv = Text::CSV_XS->new({ binary => 1 });
47              
48 3 50       518 open my $fh, '<', $file
49             or croak "Can't read $file: $!";
50              
51 3         125 while (my $row = $csv->getline($fh)) {
52 336         7360 my $chord = shift @$row;
53 336         400 my $fingering = shift @$row;
54 336         360 push @{ $data{$chord}{fingering} }, $fingering;
  336         703  
55 336         396 my @notes;
56 336         445 for my $r (@$row) {
57 2016 100       3390 push @notes, $r if $r ne '';
58             }
59 336         367 push @{ $data{$chord}{notes} }, \@notes;
  336         5698  
60             }
61              
62 3         124 close $fh;
63              
64 3         53 return \%data;
65             }
66              
67              
68             sub transform {
69 8     8 1 8431 my ($self, $target, $chord_name, $variation) = @_;
70              
71 8         31 $target = $self->pitchnum($target);
72 8 100       379 croak 'Invalid note' unless $target;
73              
74 7   50     13 $chord_name //= '';
75              
76 7         11 my @notes;
77              
78 7 100       13 if (defined $variation) {
79 6         111 my $pitches = $self->chords->{ 'C' . $chord_name }{notes}[$variation];
80              
81 6         43 my $diff = $target - _lowest_c($pitches);
82              
83 6         13 @notes = map { $_ + $diff } @$pitches;
  18         29  
84             }
85             else {
86 1         3 for my $pitches (@{ $self->chords->{ 'C' . $chord_name }{notes} }) {
  1         20  
87 3         14 my $diff = $target - _lowest_c($pitches);
88 3         6 push @notes, [ map { $_ + $diff } @$pitches ];
  15         22  
89             }
90             }
91              
92 7         24 return \@notes;
93             }
94              
95             sub _lowest_c {
96 27     27   36 my ($pitches) = @_;
97              
98 27         37 my $lowest = 0;
99              
100 27         127 for my $c (48, 60, 72) {
101 37 100   53   149 if (any { $_ == $c } @$pitches) {
  53         103  
102 25         29 $lowest = $c;
103 25         31 last;
104             }
105             }
106              
107 27         64 return $lowest;
108             }
109              
110              
111             sub voicings {
112 3     3 1 5097 my ($self, $chord_name, $format) = @_;
113              
114 3   50     9 $chord_name //= '';
115 3   100     10 $format ||= '';
116              
117 3         61 my $voicings = $self->chords->{ 'C' . $chord_name }{notes};
118              
119 3 100       20 if ($format) {
120 2         2 my $temp;
121              
122 2         4 for my $chord (@$voicings) {
123 6         9 my $span;
124              
125 6         11 for my $n (@$chord) {
126 30         58 my $note = Music::Note->new($n, 'midinum')->format($format);
127 30         1042 push @$span, $note;
128             }
129              
130 6         8 push @$temp, $span;
131             }
132              
133 2         5 $voicings = $temp;
134             }
135              
136 3         9 return $voicings;
137             }
138              
139              
140             sub fingering {
141 10     10 1 7373 my ($self, $target, $chord_name, $variation) = @_;
142              
143 10         32 $target = $self->pitchnum($target);
144              
145 10   50     465 $chord_name //= '';
146              
147 10         13 my @fingering;
148              
149 10 100       20 if (defined $variation) {
150 8         157 my $fingering = $self->chords->{ 'C' . $chord_name }{fingering}[$variation];
151 8         168 my $pitches = $self->chords->{ 'C' . $chord_name }{notes}[$variation];
152              
153 8         55 my ($str, $p) = _find_fingering($target, $pitches, $fingering);
154              
155 8 50       29 push @fingering, $str . '-' . $p if $p >= 0;
156             }
157             else {
158 2         36 for (zip $self->chords->{ 'C' . $chord_name }{notes}, $self->chords->{ 'C' . $chord_name }{fingering}) {
159 10         44 my ($pitches, $fingering) = @$_;
160              
161 10         14 my ($str, $p) = _find_fingering($target, $pitches, $fingering);
162              
163 10 100       27 push @fingering, $str . '-' . $p if $p >= 0;
164             }
165             }
166              
167 10         27 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         27 my $diff = $target - _lowest_c($pitches);
175              
176 18         45 my ($str, $pos) = split /-/, $fingering;
177              
178 18         28 my $p = $pos + $diff;
179              
180 18 100 66     70 if ($pos != 1 && $str !~ /0/) {
    100 66        
181 12 100 66     44 if ($p == 0 && $str !~ /0/) {
    50 33        
182 4         8 $str = _decrement_fingering($str);
183 4         7 $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         36 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       31 $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         6 my $decremented = '';
210 4         11 for my $char (split //, $fingering) {
211 24 100       57 $decremented .= $char =~ /\d/ ? $char - 1 : $char;
212             }
213 4         10 return $decremented;
214             }
215              
216             1;
217              
218             __END__