File Coverage

blib/lib/Music/FretboardDiagram.pm
Criterion Covered Total %
statement 105 168 62.5
branch 28 84 33.3
condition 5 15 33.3
subroutine 18 20 90.0
pod 2 3 66.6
total 158 290 54.4


line stmt bran cond sub pod time code
1             package Music::FretboardDiagram;
2             our $AUTHORITY = 'cpan:GENE';
3              
4             # ABSTRACT: Draw fretboard chord diagrams
5              
6             our $VERSION = '0.1315';
7              
8 1     1   1027 use Moo;
  1         8943  
  1         3  
9 1     1   1539 use strictures 2;
  1         1319  
  1         31  
10 1     1   531 use namespace::clean;
  1         8980  
  1         6  
11              
12 1     1   241 use Carp 'croak';
  1         2  
  1         34  
13 1     1   731 use Imager ();
  1         45504  
  1         26  
14 1     1   417 use List::SomeUtils 'first_index';
  1         5212  
  1         60  
15 1     1   375 use Music::Chord::Namer 'chordname';
  1         2047  
  1         50  
16              
17 1     1   6 use constant WHITE => 'white';
  1         2  
  1         48  
18 1     1   5 use constant BLACK => 'black';
  1         1  
  1         49  
19 1     1   5 use constant TAN => 'tan';
  1         1  
  1         2470  
20              
21              
22             has chord => (
23             is => 'rw',
24             );
25              
26              
27             has position => (
28             is => 'rw',
29             isa => \&_positive_int,
30             default => sub { 1 },
31             );
32              
33              
34             has absolute => (
35             is => 'ro',
36             isa => \&_boolean,
37             default => sub { 0 },
38             );
39              
40              
41             has strings => (
42             is => 'ro',
43             isa => \&_positive_int,
44             default => sub { 6 },
45             );
46              
47              
48             has frets => (
49             is => 'ro',
50             isa => \&_positive_int,
51             default => sub { 5 },
52             );
53              
54              
55             has size => (
56             is => 'ro',
57             isa => \&_positive_int,
58             default => sub { 30 },
59             );
60              
61              
62             has outfile => (
63             is => 'rw',
64             default => sub { 'chord-diagram' },
65             );
66              
67              
68             has type => (
69             is => 'ro',
70             default => sub { 'png' },
71             );
72              
73              
74             has font => (
75             is => 'ro',
76             default => sub { '/usr/share/fonts/truetype/freefont/FreeMono.ttf' },
77             );
78              
79              
80             has tuning => (
81             is => 'ro',
82             default => sub { [qw/E B G D A E/] },
83             );
84              
85              
86             has horiz => (
87             is => 'ro',
88             isa => \&_boolean,
89             default => sub { 0 },
90             );
91              
92              
93             has image => (
94             is => 'ro',
95             isa => \&_boolean,
96             default => sub { 0 },
97             );
98              
99              
100             has string_color => (
101             is => 'ro',
102             default => sub { 'blue' },
103             );
104              
105              
106             has fret_color => (
107             is => 'ro',
108             default => sub { 'darkgray' },
109             );
110              
111              
112             has dot_color => (
113             is => 'ro',
114             default => sub { 'black' },
115             );
116              
117              
118             has showname => (
119             is => 'rw',
120             default => sub { 1 },
121             );
122              
123              
124             has verbose => (
125             is => 'ro',
126             isa => \&_boolean,
127             default => sub { 0 },
128             );
129              
130              
131             has fretboard => (
132             is => 'ro',
133             init_arg => undef,
134             );
135              
136              
137             sub BUILD {
138 5     5 0 32 my ( $self, $args ) = @_;
139              
140 5 50       84 $self->chord( [ [ $self->position, $self->chord ] ] )
141             unless ref $self->chord;
142              
143 5 100 66     60 croak 'chord length and string number differ'
144             if $self->chord && length( $self->chord->[0][1] ) != $self->strings;
145              
146 4         13 my @scale = qw/C Db D Eb E F Gb G Ab A Bb B/;
147              
148             # Make a scale position index corresponding to the given tuning
149 4     172   5 my @index = map { my $t = $_; first_index { $t eq $_ } @scale } @{ $self->tuning };
  24         33  
  24         60  
  172         207  
  4         9  
150              
151 4         6 my %notes;
152              
153 4         6 my $string = 0;
154 4         6 for my $i ( @index ) {
155             # Make a scale note list for the string
156 24         39 $notes{++$string} = [ map { $scale[ ($i + $_) % @scale ] } 0 .. @scale - 1 ];
  288         426  
157             }
158              
159 4         22 $self->{fretboard} = \%notes;
160             }
161              
162              
163             sub draw {
164 1     1 1 368 my ($self) = @_;
165              
166 1 50       5 if ( $self->horiz ) {
167 0         0 return $self->_draw_horiz;
168             }
169              
170 1         3 my $SPACE = $self->size;
171              
172 1         3 my $frets = $self->frets + 1;
173 1         1 my $font;
174              
175             # Setup a new image
176 1         7 my $i = Imager->new(
177             xsize => $SPACE + $self->strings * $SPACE,
178             ysize => $SPACE + $frets * $SPACE,
179             );
180 1         200 $i->box( filled => 1, color => WHITE );
181              
182 1 50       8762 if ( -e $self->font ) {
183 0         0 $font = Imager::Font->new( file => $self->font );
184             }
185             else {
186 1         45 warn 'WARNING: Font ', $self->font, " not found\n";
187             }
188              
189             # Draw the horizontal fret lines
190 1         8 for my $fret ( 0 .. $frets - 1 ) {
191 6         5947 $i->line(
192             color => $self->fret_color,
193             x1 => $SPACE,
194             y1 => $SPACE + $fret * $SPACE,
195             x2 => $SPACE + ($self->strings - 1) * $SPACE,
196             y2 => $SPACE + $fret * $SPACE,
197             aa => 1,
198             endp => 1
199             );
200              
201             # Indicate the neck position
202 6 100       792 if ( $fret == 1 ) {
203 1         9 $i->string(
204             font => $font,
205             text => $self->chord->[0][0],
206             color => BLACK,
207             x => $SPACE / 4,
208             y => $SPACE * 2 + $SPACE / 4,
209             size => $SPACE / 2,
210             aa => 1,
211             );
212             }
213              
214 6 100       38 if ( $self->_fret_match($fret) ) {
215 2 50       42 $i->circle(
216             color => TAN,
217             r => $SPACE / 8,
218             x => $SPACE * $self->strings / 2 + $SPACE / 2,
219             y => $SPACE + $fret * $SPACE + $SPACE / 2,
220             ) if ( $SPACE + $fret * $SPACE + $SPACE / 2 ) < ( $SPACE * $frets );
221             }
222             }
223              
224             # Draw the vertical string lines
225 1         140 for my $string ( 0 .. $self->strings - 1 ) {
226 6         622 $i->line(
227             color => $self->string_color,
228             x1 => $SPACE + $string * $SPACE,
229             y1 => $SPACE,
230             x2 => $SPACE + $string * $SPACE,
231             y2 => $SPACE + ($frets - 1) * $SPACE,
232             aa => 1,
233             endp => 1
234             );
235             }
236              
237 1         114 for my $spec ( @{ $self->chord } ) {
  1         4  
238 1         2 my ( $posn, $chord ) = @$spec;
239              
240 1         2 my @chord;
241              
242             # Draw the note/mute markers
243 1         2 my $string = $self->strings;
244 1         4 for my $note ( split //, $chord ) {
245 6 50       13 if ( $note =~ /-/ ) {
246 0         0 $string--;
247 0         0 next;
248             }
249              
250 6 50       16 if ( $note =~ /[xX]/ ) {
    0          
251 6 50       11 warn "X at 0,$string\n" if $self->verbose;
252              
253 6         19 $i->string(
254             font => $font,
255             text => 'X',
256             color => BLACK,
257             x => $SPACE + ($self->strings - $string) * $SPACE - $SPACE / 6,
258             y => $SPACE - 2,
259             size => $SPACE / 2,
260             aa => 1,
261             );
262             }
263             elsif ( $note =~ /[oO0]/ ) {
264 0         0 my $temp = $self->_note_at(0, $string, $note);
265 0         0 push @chord, $temp;
266              
267 0 0       0 warn "O at 0,$string = $temp\n" if $self->verbose;
268              
269 0         0 $i->string(
270             font => $font,
271             text => 'O',
272             color => BLACK,
273             x => $SPACE + ($self->strings - $string) * $SPACE - $SPACE / 6,
274             y => $SPACE - 2,
275             size => $SPACE / 2,
276             aa => 1,
277             );
278             }
279             else {
280 0         0 my $temp = $self->_note_at($posn, $string, $note);
281 0         0 push @chord, $temp;
282              
283 0 0       0 warn "Dot at $note,$string = $temp\n" if $self->verbose;
284              
285 0 0       0 my $y = $self->absolute
286             ? $SPACE + $SPACE / 2 + ($posn - 1 + $note - 1) * $SPACE
287             : $SPACE + $SPACE / 2 + ($note - 1) * $SPACE;
288              
289 0 0 0     0 $i->circle(
290             color => $self->dot_color,
291             r => $SPACE / 5,
292             x => $SPACE + ($self->strings - $string) * $SPACE,
293             y => $y,
294             ) if $y >= $SPACE && $y <= $SPACE * $frets;
295             }
296              
297             # Decrement the current string number
298 6         103 $string--;
299             }
300              
301             # Print the chord name if requested
302 1 50       4 if ( $self->showname ) {
303 1 50       7 my $chord_name = $self->showname eq '1' ? chordname(@chord) : $self->showname;
304 1 50       700 warn "Chord = $chord_name\n" if $self->verbose;
305 1         5 $i->string(
306             font => $font,
307             text => $chord_name,
308             color => BLACK,
309             x => $SPACE,
310             y => ($frets + 1) * $SPACE - $SPACE / 3,
311             size => $SPACE / 2,
312             aa => 1,
313             );
314             }
315             }
316              
317 1 50       22 if ( $self->image ) {
318 1         2 return $i;
319             }
320             else {
321 0         0 $self->_output_image($i);
322             }
323             }
324              
325             sub _draw_horiz {
326 0     0   0 my ($self) = @_;
327              
328 0         0 my $SPACE = $self->size;
329              
330 0         0 my $frets = $self->frets + 1;
331 0         0 my $font;
332              
333             # Setup a new image
334 0         0 my $i = Imager->new(
335             ysize => $SPACE + $self->strings * $SPACE,
336             xsize => $SPACE + $frets * $SPACE,
337             );
338 0         0 $i->box( filled => 1, color => WHITE );
339              
340 0 0       0 if ( -e $self->font ) {
341 0         0 $font = Imager::Font->new( file => $self->font );
342             }
343             else {
344 0         0 warn 'WARNING: Font ', $self->font, " not found\n";
345             }
346              
347             # Draw the vertical fret lines
348 0         0 for my $fret ( 0 .. $frets - 1 ) {
349 0         0 $i->line(
350             color => $self->fret_color,
351             y1 => $SPACE,
352             x1 => $SPACE + $fret * $SPACE,
353             y2 => $SPACE + ($self->strings - 1) * $SPACE,
354             x2 => $SPACE + $fret * $SPACE,
355             aa => 1,
356             endp => 1
357             );
358              
359             # Indicate the neck position
360 0 0       0 if ( $fret == 1 ) {
361 0         0 $i->string(
362             font => $font,
363             text => $self->chord->[0][0],
364             color => BLACK,
365             y => $SPACE / 2 + $SPACE / 5,
366             x => $SPACE * 2 - $SPACE / 5,
367             size => $SPACE / 2,
368             aa => 1,
369             );
370             }
371              
372 0 0       0 if ( $self->_fret_match($fret) ) {
373 0 0       0 $i->circle(
374             color => TAN,
375             r => $SPACE / 8,
376             y => $SPACE * $self->strings / 2 + $SPACE / 2,
377             x => $SPACE + $fret * $SPACE + $SPACE / 2,
378             ) if ( $SPACE + $fret * $SPACE + $SPACE / 2 ) < ( $SPACE * $frets );
379             }
380             }
381              
382             # Draw the horizontal string lines
383 0         0 for my $string ( 0 .. $self->strings - 1 ) {
384 0         0 $i->line(
385             color => $self->string_color,
386             y1 => $SPACE + $string * $SPACE,
387             x1 => $SPACE,
388             y2 => $SPACE + $string * $SPACE,
389             x2 => $SPACE + ($frets - 1) * $SPACE,
390             aa => 1,
391             endp => 1
392             );
393             }
394              
395 0         0 for my $spec ( @{ $self->chord } ) {
  0         0  
396 0         0 my ( $posn, $chord ) = @$spec;
397              
398 0         0 my @chord;
399              
400             # Draw the note/mute markers
401 0         0 my $string = 1;
402 0         0 for my $note ( reverse split //, $chord ) {
403 0 0       0 if ( $note =~ /-/ ) {
404 0         0 $string++;
405 0         0 next;
406             }
407              
408 0 0       0 if ( $note =~ /[xX]/ ) {
    0          
409 0 0       0 warn "X at posn: $posn, fret:0, string:$string\n" if $self->verbose;
410              
411 0         0 $i->string(
412             font => $font,
413             text => 'X',
414             color => BLACK,
415             y => $SPACE + ($string - 1) * $SPACE + $SPACE / 4,
416             x => $SPACE - $SPACE / 2,
417             size => $SPACE / 2,
418             aa => 1,
419             );
420             }
421             elsif ( $note =~ /[oO0]/ ) {
422 0         0 my $temp = $self->_note_at(0, $string, $note);
423 0         0 unshift @chord, $temp;
424              
425 0 0       0 warn "O at posn: $posn, fret:0, string:$string = $temp\n" if $self->verbose;
426              
427 0         0 $i->string(
428             font => $font,
429             text => 'O',
430             color => BLACK,
431             y => $SPACE + ($string - 1) * $SPACE + $SPACE / 4,
432             x => $SPACE - $SPACE / 2,
433             size => $SPACE / 2,
434             aa => 1,
435             );
436             }
437             else {
438 0         0 my $temp = $self->_note_at($posn, $string, $note);
439 0         0 unshift @chord, $temp;
440              
441 0 0       0 warn "Dot at posn: $posn, fret:$note, string:$string = $temp\n" if $self->verbose;
442              
443 0 0       0 my $x = $self->absolute
444             ? $SPACE + $SPACE / 2 + ($posn - 1 + $note - 1) * $SPACE
445             : $SPACE + $SPACE / 2 + ($note - 1) * $SPACE;
446              
447 0 0 0     0 $i->circle(
448             color => $self->dot_color,
449             r => $SPACE / 5,
450             x => $x,
451             y => $SPACE + ($string - 1) * $SPACE,
452             ) if $x >= $SPACE && $x <= $SPACE * $frets;
453             }
454              
455             # Increment the current string number
456 0         0 $string++;
457             }
458              
459             # Print the chord name if requested
460 0 0       0 if ( $self->showname ) {
461 0 0       0 my $chord_name = $self->showname eq '1' ? chordname(@chord) : $self->showname;
462 0 0       0 warn "Chord = $chord_name\n" if $self->verbose;
463 0         0 $i->string(
464             font => $font,
465             text => $chord_name,
466             color => BLACK,
467             x => $SPACE,
468             y => ($self->strings + 1) * $SPACE - $SPACE / 3,
469             size => $SPACE / 2,
470             aa => 1,
471             );
472             }
473             }
474              
475 0 0       0 if ( $self->image ) {
476 0         0 return $i;
477             }
478             else {
479 0         0 $self->_output_image($i);
480             }
481             }
482              
483             sub _fret_match {
484 6     6   11 my ($self, $fret) = @_;
485 6   33     107 return ( $self->position + $fret == 3 )
486             || ( $self->position + $fret == 5 )
487             || ( $self->position + $fret == 7 )
488             || ( $self->position + $fret == 9 )
489             || ( $self->position + $fret == 12 )
490             || ( $self->position + $fret == 15 )
491             || ( $self->position + $fret == 17 )
492             || ( $self->position + $fret == 19 )
493             || ( $self->position + $fret == 21 )
494             || ( $self->position + $fret == 24 );
495             }
496              
497             sub _note_at {
498 16     16   2823 my ($self, $posn, $string, $n) = @_;
499 16         20 my $i;
500 16 100       26 if ($posn) {
501 12         17 $i = ($posn + $n - 1) % @{ $self->fretboard->{1} };
  12         24  
502             }
503             else {
504 4         5 $i = 0;
505             }
506 16         50 return $self->fretboard->{$string}[$i];
507             }
508              
509             sub _output_image {
510 0     0   0 my ($self, $img) = @_;
511 0         0 my $name = $self->outfile . '.' . $self->type;
512 0 0       0 $img->write( type => $self->type, file => $name )
513             or croak "Can't save $name: ", $img->errstr;
514             }
515              
516             sub _positive_int {
517 37     37   815 my ($arg) = @_;
518 37 100       495 croak "$arg is not a positive integer" unless $arg =~ /^[1-9]\d*$/;
519             }
520              
521             sub _boolean {
522 40     40   1131 my ($arg) = @_;
523 40 100       524 croak "$arg is not a Boolean value" unless $arg =~ /^[10]$/;
524             }
525              
526              
527             sub spec_to_notes {
528 3     3 1 827 my ($self, $spec) = @_;
529              
530 3         6 my @notes;
531              
532 3 50       17 croak 'chord length and string number differ'
533             if length $spec != $self->strings;
534              
535 3         7 my $string = $self->strings;
536              
537 3         10 for my $n (split //, $spec) {
538 18 100 66     39 if ($n eq 'x' || $n eq 'X') {
539 8         8 $string--;
540 8         12 next;
541             }
542              
543 10 100       25 if ( $n =~ /[oO0]/ ) {
544 4         8 push @notes, $self->_note_at(0, $string, $n);
545             }
546             else {
547 6         87 push @notes, $self->_note_at($self->position, $string, $n);
548             }
549              
550 10         14 $string--;
551             }
552              
553 3         10 return \@notes;
554             }
555              
556             1;
557              
558             __END__