2015-11-19 15:40:59 -08:00
|
|
|
package DDG::Goodie::ChordDiagrams;
|
2015-02-25 14:29:01 -08:00
|
|
|
# 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-01-20 14:15:25 -08:00
|
|
|
use JSON;
|
2016-02-29 18:39:03 -08:00
|
|
|
use List::Util qw(min);
|
2016-03-09 15:22:51 -08:00
|
|
|
use File::Slurp qw(read_file);
|
2015-12-21 16:55:02 -08:00
|
|
|
# docs: http://search.cpan.org/~ronan/SVG-2.33/lib/SVG/Manual.pm
|
2015-02-25 14:29:01 -08:00
|
|
|
|
2015-11-19 15:40:59 -08:00
|
|
|
zci answer_type => "chord_diagrams";
|
2015-02-25 14:29:01 -08:00
|
|
|
zci is_cached => 1;
|
|
|
|
|
2015-02-25 17:04:15 -08:00
|
|
|
triggers any => "chord", "tab";
|
2015-02-25 14:29:01 -08:00
|
|
|
|
2016-01-20 14:15:25 -08:00
|
|
|
local $/;
|
2016-03-09 15:22:51 -08:00
|
|
|
|
2015-11-03 15:54:45 -08:00
|
|
|
# Store the instruments that the program will respond to, with a
|
2015-02-25 14:29:01 -08:00
|
|
|
# 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-29 17:17:20 -07:00
|
|
|
guitar => {
|
|
|
|
chords => decode_json(read_file(share('guitar.json'))),
|
|
|
|
strings => 6
|
|
|
|
},
|
|
|
|
ukulele => {
|
|
|
|
chords => decode_json(read_file(share('ukulele.json'))),
|
|
|
|
strings => 4
|
|
|
|
}
|
2015-02-25 14:29:01 -08: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(
|
|
|
|
x1=>$x - $size/2,
|
|
|
|
y1=>$y - $size/2,
|
|
|
|
x2=>$x + $size/2,
|
|
|
|
y2=>$y + $size/2,
|
|
|
|
style=>{
|
|
|
|
'stroke'=>'black',
|
|
|
|
'stroke-width'=>'2'
|
|
|
|
});
|
|
|
|
|
|
|
|
$svg->line(
|
|
|
|
x1=>$x - $size/2,
|
|
|
|
y1=>$y + $size/2,
|
|
|
|
x2=>$x + $size/2,
|
|
|
|
y2=>$y - $size/2,
|
|
|
|
style=>{
|
|
|
|
'stroke'=>'black',
|
|
|
|
'stroke-width'=>'2'
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2015-12-03 18:43:39 -08:00
|
|
|
# Generate chord SVG
|
|
|
|
sub gen_svg {
|
|
|
|
my (%opts) = @_;
|
|
|
|
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;
|
|
|
|
|
2016-03-02 17:07:12 -08:00
|
|
|
my @t = grep {$_ != -1} @{$opts{"points"}};
|
|
|
|
if ((my $m = min @t) > 2) {
|
|
|
|
$start = $m - 1;
|
2016-02-29 18:39:03 -08:00
|
|
|
|
2016-03-02 17:07:12 -08:00
|
|
|
$svg->text(
|
|
|
|
style => {
|
|
|
|
'font' => 'Arial',
|
|
|
|
'font-size' => '14',
|
|
|
|
},
|
|
|
|
x => -15,
|
|
|
|
y => $top_pad + 5
|
|
|
|
)->cdata($start);
|
|
|
|
}
|
2016-02-29 18:39:03 -08:00
|
|
|
if($start == 0) {
|
|
|
|
$svg->line(
|
|
|
|
x1=>0,
|
|
|
|
y1=>$top_pad,
|
|
|
|
x2=>$opts{"width"},
|
|
|
|
y2=>$top_pad,
|
|
|
|
style=>{
|
|
|
|
'stroke'=>'black',
|
|
|
|
'stroke-width'=>'4'
|
|
|
|
});
|
|
|
|
}
|
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(
|
2015-12-03 19:21:56 -08:00
|
|
|
x1=>0,
|
2016-01-20 14:15:25 -08:00
|
|
|
y1=>$top_pad + 2 + $i * $fret_dist,
|
2015-12-03 19:21:56 -08:00
|
|
|
x2=>$opts{"width"},
|
2016-01-20 14:15:25 -08:00
|
|
|
y2=>$top_pad + 2 + $i * $fret_dist,
|
2015-12-03 19:21:56 -08:00
|
|
|
style=>{
|
|
|
|
'stroke'=>'black',
|
2016-01-20 14:15:25 -08:00
|
|
|
'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-01-20 14:15:25 -08: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'=>'black',
|
|
|
|
'stroke-width'=>'2'
|
|
|
|
});
|
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"});
|
2015-12-04 16:23:19 -08:00
|
|
|
my $fill = 'black';
|
|
|
|
$fill = 'none' if ($p == 0);
|
2016-01-20 14:15:25 -08:00
|
|
|
if ($p == -1) {
|
|
|
|
mk_x($svg,
|
|
|
|
$i * $p_dist + 1,
|
|
|
|
$top_pad - $fret_dist/2 + 1,
|
|
|
|
10)
|
|
|
|
} else {
|
|
|
|
$svg->circle(
|
2015-12-04 16:23:19 -08:00
|
|
|
cx=>$i * $p_dist + 1,
|
2016-02-29 18:39:03 -08:00
|
|
|
cy=>$top_pad + $fret_dist * ($p - $start) - $fret_dist/2 + 2,
|
2015-12-04 16:23:19 -08:00
|
|
|
r=>5,
|
|
|
|
style=>{
|
|
|
|
'stroke'=>'black',
|
|
|
|
'stroke-width'=>2,
|
|
|
|
'fill'=>$fill
|
|
|
|
});
|
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;
|
|
|
|
};
|
|
|
|
|
2015-11-03 15:54:45 -08: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
|
2015-02-25 14:29:01 -08:00
|
|
|
# chords and instrument hashes
|
|
|
|
sub items{
|
|
|
|
my @words = split(" ", lc $_[0]);
|
|
|
|
$_[0] = join("sharp", split("#", $_[0]));
|
2016-01-20 16:42:21 -08:00
|
|
|
my ($temp, $key, $mod, $chord, $dom, $temp2) = /( |^)([a-g])(sharp|b|)(m|min|minor|M|maj|major|sus[24]|aug9?|)(5|7|9|11|13|)( |$)/i ;
|
2015-02-25 14:29:01 -08: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;
|
|
|
|
}
|
2015-11-19 13:09:39 -08:00
|
|
|
my %mod_hash = (sharp => 1, b => -1);
|
2016-03-29 17:56:36 -07:00
|
|
|
if(defined $mod) {
|
|
|
|
$mod = $mod_hash{$mod} || 0;
|
|
|
|
} else {
|
|
|
|
$mod = 0;
|
2015-11-19 13:09:39 -08:00
|
|
|
}
|
2016-03-29 17:56:36 -07:00
|
|
|
|
|
|
|
if(!defined $key) {
|
|
|
|
$key = "";
|
|
|
|
}
|
|
|
|
|
2016-01-20 15:27:15 -08:00
|
|
|
if(defined $chord && ($chord eq "m" || $chord =~ /(min|minor)/i)){
|
2016-01-20 14:29:01 -08:00
|
|
|
$chord = "min";
|
2015-11-07 15:03:36 -08:00
|
|
|
}elsif(defined $chord && ($chord eq "M" || $chord =~ /(maj|major)/i)){
|
2016-01-20 14:29:01 -08:00
|
|
|
$chord = "maj";
|
2015-11-07 15:03:36 -08:00
|
|
|
}elsif(defined $chord && $chord =~ /sus[24]/i){
|
2015-11-19 13:09:39 -08:00
|
|
|
$chord = lc $chord;
|
2016-01-20 16:42:21 -08:00
|
|
|
}elsif(defined $chord && $chord =~ /aug/i){
|
|
|
|
$chord = lc $chord;
|
2015-02-25 14:29:01 -08:00
|
|
|
}elsif($dom){
|
|
|
|
$chord = "dominant";
|
|
|
|
}else{
|
2016-01-20 14:29:01 -08:00
|
|
|
$chord = "maj";
|
2015-02-25 14:29:01 -08:00
|
|
|
};
|
|
|
|
if(!$dom){
|
2016-01-20 16:42:21 -08:00
|
|
|
$dom = "";
|
2015-02-25 14:29:01 -08:00
|
|
|
};
|
2016-03-19 07:42:45 -07:00
|
|
|
my $instr;
|
2016-03-19 07:48:48 -07:00
|
|
|
foreach my $i (keys %instruments) {
|
2016-03-19 07:42:45 -07:00
|
|
|
if(grep(/^$i$/, @words)) {
|
|
|
|
$instr = $i;
|
|
|
|
last;
|
|
|
|
}
|
|
|
|
}
|
2016-01-20 15:27:15 -08:00
|
|
|
return $instr, $chord, uc $key, $mod, $dom;
|
2015-02-25 14:29:01 -08:00
|
|
|
};
|
|
|
|
|
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-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-01-20 15:27:15 -08:00
|
|
|
return undef;
|
2016-01-20 14:15:25 -08:00
|
|
|
};
|
2015-02-25 14:29:01 -08:00
|
|
|
|
|
|
|
# Handle statement
|
|
|
|
handle remainder => sub {
|
|
|
|
my ($instr_name, $chord_name, $key_name, $mod, $dom) = items($_);
|
2015-11-07 15:03:36 -08:00
|
|
|
if((defined $instr_name) && (defined $chord_name) && (defined $key_name)){
|
2016-03-19 07:48:48 -07:00
|
|
|
my $strings = $instruments{$instr_name}{"strings"};
|
2016-01-20 14:29:01 -08:00
|
|
|
my $length = 4;
|
2016-01-20 16:44:23 -08:00
|
|
|
my $input = join(" ", (uc $key_name) . (($mod == -1)? "b" :(($mod == 1)? "#" : "" )), $chord_name . $dom, "guitar chord");
|
2015-11-19 15:28:17 -08:00
|
|
|
|
2016-01-20 15:27:15 -08:00
|
|
|
if ($mod == -1) {
|
|
|
|
$mod = 'b';
|
|
|
|
} elsif ($mod == 1) {
|
|
|
|
$mod = '#'
|
|
|
|
} else {
|
|
|
|
$mod = '';
|
|
|
|
}
|
2016-03-19 07:42:45 -07:00
|
|
|
|
2016-03-19 07:48:48 -07:00
|
|
|
my $r = get_chord($key_name . $mod, $chord_name . $dom, $instruments{$instr_name}{"chords"});
|
2016-03-19 07:42:45 -07:00
|
|
|
|
2016-01-20 16:42:21 -08:00
|
|
|
return if not defined $r;
|
2016-02-16 12:12:49 -08:00
|
|
|
my @results = @{$r};
|
|
|
|
@results = map {
|
2016-03-29 17:17:20 -07:00
|
|
|
svg => gen_svg(
|
|
|
|
'width'=>100,
|
|
|
|
'height'=>120,
|
|
|
|
'frets'=>$length,
|
|
|
|
'strings'=>$strings,
|
|
|
|
'points'=> $_,
|
|
|
|
)->xmlify,
|
2016-02-16 12:12:49 -08:00
|
|
|
}, @results;
|
2016-01-20 14:29:01 -08:00
|
|
|
return 'chord_diagrams', structured_answer => {
|
|
|
|
id => 'chord_diagrams',
|
|
|
|
name => 'Music',
|
2016-02-16 12:12:49 -08:00
|
|
|
data => \@results,
|
2016-01-20 14:29:01 -08:00
|
|
|
templates => {
|
2016-02-16 11:53:25 -08:00
|
|
|
detail => 0,
|
|
|
|
item => 'base_item',
|
2016-01-20 14:29:01 -08:00
|
|
|
options => {
|
2016-03-28 17:24:37 -07:00
|
|
|
url => "www.ddg.gg",
|
2016-01-20 14:29:01 -08:00
|
|
|
content => 'DDH.chord_diagrams.detail'
|
2016-02-16 11:53:25 -08:00
|
|
|
},
|
|
|
|
variants => {
|
|
|
|
tile => 'narrow'
|
2016-01-20 14:29:01 -08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
meta => {}
|
2015-11-19 15:28:17 -08:00
|
|
|
};
|
2015-02-25 14:29:01 -08:00
|
|
|
};
|
2015-11-07 15:03:36 -08:00
|
|
|
return;
|
2015-02-25 14:29:01 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
1;
|