File Coverage

blib/lib/MIDI/Chord/Guitar.pm
Criterion Covered Total %
statement 116 121 95.8
branch 25 28 89.2
condition 12 20 60.0
subroutine 18 19 94.7
pod 3 3 100.0
total 174 191 91.1


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