zeroclickinfo-goodies/lib/DDG/Goodie/ChordDiagrams.pm

243 lines
6.4 KiB
Perl
Raw Normal View History

2015-11-19 15:40:59 -08:00
package DDG::Goodie::ChordDiagrams;
# ABSTRACT: For getting the fingering for chords on popular strings instruments
use DDG::Goodie;
2015-12-03 18:43:39 -08:00
use SVG;
2016-03-31 17:06:33 -07:00
use JSON::MaybeXS;
2016-02-29 18:39:03 -08:00
use List::Util qw(min);
2015-12-21 16:55:02 -08:00
# docs: http://search.cpan.org/~ronan/SVG-2.33/lib/SVG/Manual.pm
2015-11-19 15:40:59 -08:00
zci answer_type => "chord_diagrams";
zci is_cached => 1;
triggers any => "chord", "tab", "chords", "tabs";
# Store the instruments that the program will respond to, with a
# list storing the note of each string in order. (Add one to note
# for sharps, and subtract one for flats)
2016-03-19 07:48:48 -07:00
my %instruments = (
2016-03-31 17:06:33 -07:00
guitar => {
chords => decode_json(share('guitar.json')->slurp),
strings => 6,
names => 'guitar'
2016-03-31 17:06:33 -07:00
},
ukulele => {
chords => decode_json(share('ukulele.json')->slurp),
strings => 4,
names => 'ukulele|uke'
2016-03-31 17:06:33 -07:00
}
);
2016-01-20 14:15:25 -08:00
# create svg X for muted strings
sub mk_x {
my $svg = shift;
my $x = shift;
my $y = shift;
my $size = shift;
$svg->line(
2016-03-31 17:06:33 -07:00
x1 => $x - $size/2,
y1 => $y - $size/2,
x2 => $x + $size/2,
y2 => $y + $size/2,
style => {
2016-01-20 14:15:25 -08:00
'stroke-width'=>'2'
});
$svg->line(
2016-03-31 17:06:33 -07:00
x1 => $x - $size/2,
y1 => $y + $size/2,
x2 => $x + $size/2,
y2 => $y - $size/2,
style => {
'stroke-width' => '2'
2016-01-20 14:15:25 -08:00
});
};
2015-12-03 18:43:39 -08:00
# Generate chord SVG
sub gen_svg {
my (%opts) = @_;
2016-03-31 17:06:33 -07:00
my $svg = SVG->new(width => $opts{"width"}, height => $opts{"height"});
2016-02-29 18:39:03 -08:00
my $top_pad = 20;
my $start = 0;
my @t = grep {$_ != -1} @{$opts{"points"}};
2016-03-31 17:38:54 -07:00
if ((my $m = min @t) > 2) {
$start = $m - 1;
2016-02-29 18:39:03 -08:00
$svg->text(
2016-03-31 17:38:54 -07:00
style => {
'font' => 'Arial',
'font-size' => '14'
2016-03-31 17:38:54 -07:00
},
x => -15,
y => $top_pad + 5
)->cdata($start);
}
2016-02-29 18:39:03 -08:00
if($start == 0) {
$svg->line(
2016-03-31 17:06:33 -07:00
x1 => 0,
y1 => $top_pad,
x2 => $opts{"width"},
y2 => $top_pad,
style => {
'stroke-width' => '4'
2016-02-29 18:39:03 -08:00
});
}
2016-01-20 14:15:25 -08:00
2016-02-29 18:39:03 -08:00
# draw frets
2016-01-20 14:15:25 -08:00
my $fret_dist = (($opts{"height"} - $top_pad) / ($opts{"frets"}));
for (my $i = 0; $i < $opts{"frets"}; $i++) {
$svg->line(
2016-03-31 17:06:33 -07:00
x1 => 0,
y1 => $top_pad + 2 + $i * $fret_dist,
x2 => $opts{"width"},
y2 => $top_pad + 2 + $i * $fret_dist,
style => {
'stroke-width' => '2'
2015-12-03 19:21:56 -08:00
});
}
2016-02-29 18:39:03 -08:00
# draw strings
2015-12-03 19:21:56 -08:00
for (my $i = 0; $i < $opts{"strings"}; $i++) {
$svg->line(
2016-03-31 17:06:33 -07:00
x1 => 1 + $i * (($opts{"width"} - 2) / ($opts{"strings"} - 1)),
y1 => $top_pad,
x2 => 1 + $i * (($opts{"width"} - 2) / ($opts{"strings"} - 1)),
y2 => $opts{"height"},
style => {
'stroke-width' => '2'
2016-01-20 14:15:25 -08:00
});
2015-12-03 19:21:56 -08:00
}
2015-12-04 15:45:55 -08:00
2016-02-29 18:39:03 -08:00
# draw finger positions
2015-12-04 15:45:55 -08:00
my $i = 0;
my $p_dist = ($opts{"width"} - 2) / ($opts{"strings"} - 1);
for my $p (@{$opts{"points"}}) {
2015-12-21 16:55:02 -08:00
last if ($i >= $opts{"strings"});
2016-01-20 14:15:25 -08:00
if ($p == -1) {
2016-03-31 17:38:54 -07:00
mk_x($svg, $i * $p_dist + 1,
$top_pad - $fret_dist/2 + 1,
10);
} elsif($p == 0) {
2016-01-20 14:15:25 -08:00
$svg->circle(
2016-03-31 17:06:33 -07:00
cx => $i * $p_dist + 1,
cy => $top_pad + $fret_dist * ($p - $start) - $fret_dist/2 + 2,
r => 5,
style => {
'stroke-width' => 2,
'fill' => 'none'
});
} else {
$svg->circle(
cx => $i * $p_dist + 1,
cy => $top_pad + $fret_dist * ($p - $start) - $fret_dist/2 + 2,
r => 5,
style => {
'stroke-width' => 2
2015-12-04 16:23:19 -08:00
});
2016-01-20 14:15:25 -08:00
}
2015-12-04 15:45:55 -08:00
$i++;
}
2015-12-03 18:43:39 -08:00
return $svg;
};
2016-03-31 17:30:23 -07:00
# used in items
my %mod_hash = (sharp => '#', b => 'b');
2016-03-31 17:30:23 -07:00
# The input parser. Uses regex to find the key to put the chord in, and the
# chord if they are conjoined.
# Also greps through the input words, looking for matches within the
# chords and instrument hashes
sub items {
my @words = split(" ", lc $_[0]);
$_[0] = join("sharp", split("#", $_[0]));
my ($temp, $key, $mod, $chord, $dom, $temp2) = /( |^)(?:\s)*([a-g])(?:\s)*(sharp|b|)(?:\s)*(m|min|minor|M|maj|major|sus[24]|aug9?|)(?:\s)*(5|7|9|11|13|)(?:\s)*( |$)/i;
2016-03-31 17:23:36 -07:00
2016-03-31 17:38:54 -07:00
if(/( |^)(5|7|9)( |$)/i) { ($temp, $dom, $temp2) = /( |^)(5|7|9|11|13)( |$)/i; }
if(/( |^)(5|7|9)th( |$)/i) { ($temp, $dom, $temp2) = /( |^)(5|7|9|11|13)th( |$)/i; }
$mod = $mod ? ($mod_hash{$mod} || '') : '';
$key ||= "";
$dom ||= "";
$chord ||= "";
2016-03-29 17:56:36 -07:00
2016-03-31 17:23:36 -07:00
SWITCH: {
if ($chord eq "m" || $chord =~ /(min|minor)/i) { $chord = "min"; last SWITCH; }
if ($chord eq "M" || $chord =~ /(maj|major)/i) { $chord = "maj"; last SWITCH; }
if ($chord =~ /sus[24]/i) { $chord = lc $chord; last SWITCH; }
if ($chord =~ /aug/i) { $chord = lc $chord; last SWITCH; }
2016-01-20 14:29:01 -08:00
$chord = "maj";
2016-03-31 17:23:36 -07:00
}
2016-03-31 17:38:54 -07:00
2016-03-19 07:42:45 -07:00
my $instr;
2016-03-19 07:48:48 -07:00
foreach my $i (keys %instruments) {
if(grep(/^$instruments{$i}{"names"}$/, @words)) {
2016-03-19 07:42:45 -07:00
$instr = $i;
last;
}
}
2016-01-20 15:27:15 -08:00
return $instr, $chord, uc $key, $mod, $dom;
};
2016-01-20 14:15:25 -08:00
sub get_chord {
my $chord = shift;
2016-02-29 18:20:49 -08:00
my $mod_name = shift; # maj, 5, min, etc.
2016-03-19 07:42:45 -07:00
my $chords = shift;
2016-03-31 17:38:54 -07:00
2016-03-29 16:04:29 -07:00
foreach my $c(@$chords) {
2016-03-29 17:56:36 -07:00
my @root = @{$c->{'root'}};
2016-03-29 15:51:47 -07:00
if (grep(/^$chord$/, @root)) {
2016-03-29 17:56:36 -07:00
my @types = @{$c->{'types'}};
2016-03-29 16:04:29 -07:00
foreach my $t(@types) {
2016-03-29 17:56:36 -07:00
if($t->{'name'} eq $mod_name) {
return(\@{$t->{'variations'}});
2016-01-20 14:15:25 -08:00
}
}
}
}
2016-03-31 17:38:54 -07:00
return;
2016-01-20 14:15:25 -08:00
};
# Handle statement
handle remainder => sub {
my ($instr_name, $chord_name, $key_name, $mod, $dom) = items($_);
2016-03-31 17:06:33 -07:00
return unless $instr_name && $chord_name && $key_name;
my $strings = $instruments{$instr_name}{"strings"};
my $length = 4;
2016-03-31 17:42:43 -07:00
return unless my $r = get_chord($key_name . $mod, $chord_name . $dom, $instruments{$instr_name}{"chords"});
2016-03-19 07:42:45 -07:00
2016-03-31 17:06:33 -07:00
my @results = @{$r};
@results = map {
svg => gen_svg(
2016-03-31 17:30:23 -07:00
'width' => 100,
'height' => 120,
'frets' => $length,
'strings' => $strings,
'points' => $_,
)->xmlify,
2016-03-31 17:06:33 -07:00
}, @results;
return 'chord_diagrams', structured_answer => {
id => 'chord_diagrams',
name => 'Music',
data => \@results,
templates => {
group => "base",
detail => 0,
options => {
content => 'DDH.chord_diagrams.detail'
2016-01-20 14:29:01 -08:00
},
2016-03-31 17:06:33 -07:00
variants => {
tile => 'narrow'
}
},
};
2015-11-07 15:03:36 -08:00
return;
};
1;