#!/usr/bin/perl -w use strict; our $VERBOSE = 0; # TODO: omit syntax needs to support any note, not just 3 and 5. # TODO: write a Chord class, return instances of that, instead of just # printing note names. # TODO: figure out a way to decide which notes are optional (e.g. the 5th # in a 7th chord, or the 5th and 7th in a 9th chord). Probably need a # "weight", since the 9th chord's 7th is "more optional" than the 5th. # (at least on guitar, you're more likely to play the 5th than the # 7th, and you're not real likely to play both due to only having 6 strings) # TODO: turn this into a module. # TODO: chord generator like what chordspeller.pl does (give it some # notes, it intuits the chord). Almost none of the existing parser code # will be usable for this. The chordspeller code is very limited (it # just permutes the notes & compares them to a fixed list of chord types). # TODO: fretboard stuff # TODO: notation (treble clef). MusicXML, or generate a PNG, or what? # TODO: simplify the parser. One way to do this: before parsing, do stuff # like s/(M|major)/maj/, s/\+/aug/ maybe, I dunno. # TODO: support roman and arabic numerals. Will require the program to # determine or be told the key (e.g. IV or 4 is F in key of C). Should # take them for input and output them too. # TODO: suggest substitutions? definitely need to know the key! use Music::Chord::Namer 'chordname'; use Music::Chord::Note; use Data::Dumper; our $grammar = <<'EOF'; # terminals letter: /[A-G]/ flat: /b/ sharp: /#/ major: /(?:maj|M)/ minor: /(?:min|m)/ dimaug: /(?:dim|\+|aug|-|adim)/ sustok: /sus/ sustype: /[24]/ number: /\d\d?/ slash: m(/) addtok: m(add) omittok: /no|omit/i #omitval: /(3|5|7|9|11|13)/ #not yet omitval: /(3|5)/ # productions # Our starting rule. Unfortunately kinda ambiguous due to all the # optional stuff: chord: note triad(?) degree(?) sus(?) add(s?) bass(?) omit(s?) { print Data::Dumper::Dumper(\%item) if $::VERBOSE; \%item; } note: letter modifier(?) modifier: flat | sharp triad: major | minor | dimaug sus: slash(?) sustok sustype(?) degree: slash(?) modifier(?) number | slash(?) major number bass: slash note add: addsym number | addsym modifier number | modifier number addsym: addtok | slash omit: slash(?) omittok omitval EOF use Parse::RecDescent; our %notevals = ( C => 0, D => 2, E => 4, F => 5, G => 7, A => 9, B => 11, 1 => 0, 2 => 2, 3 => 4, 4 => 5, 5 => 7, 6 => 9, 7 => 11, 8 => 0, 9 => 2, 10 => 4, 11 => 5, 12 => 7, 13 => 9, 14 => 11, 15 => 0, ); sub mknote { my ($name, $mod) = @_; $mod ||= ''; #print Data::Dumper::Dumper($mod); warn "name '$name', mod '$mod'" if $::VERBOSE; my $val = $notevals{$name}; $val++ if $mod eq '#'; $val-- if $mod eq 'b'; return $val % 12; } sub extract_mod { my $mod = shift || return ""; if($mod) { if($mod->{flat}) { $mod = 'b'; } elsif($mod->{sharp}) { $mod = '#'; } else { $mod = ""; } } else { $mod = ""; } return $mod; } sub extract_note { my $n = shift; my $mod = extract_mod($n->{"modifier(?)"}->[0]); my $note = mknote($n->{letter}->{__PATTERN1__}, $mod); } sub get_triad_type { my $t = shift; if($t->{dimaug}) { my $da = $t->{dimaug}->{__PATTERN1__}; $da = 'aug' if $da eq '+'; $da = 'dim' if $da eq '-'; return $da; } elsif($t->{major}) { return 'major'; } elsif($t->{minor}) { return 'minor'; } else { return ''; } } our @note_names = ( 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B' ); sub get_note_name { return $note_names[$_[0] % 12]; } sub get_sus_type { my $s = shift; return 0 unless defined $s; return $s->{"sustype(?)"}->[0]->{__PATTERN1__} || 4; } sub extract_number { my $n = shift || return 0; return $n->{__PATTERN1__}; } sub create_chord { my $c = shift; my $have7 = ''; my $note = extract_note($c->{note}); warn "root note is $note" if $::VERBOSE; my $bass = $note; if($c->{"bass(?)"}->[0]) { $bass = extract_note($c->{"bass(?)"}->[0]->{note}); } warn "bass note is $bass" if $::VERBOSE; my $third = 4; my $fifth = 7; my $seventh = undef; my $seven_type = 0; # offset in semitones from a minor 7 my @ext_notes = (); my $triad_type = get_triad_type($c->{"triad(?)"}->[0]); if($triad_type eq 'major') { $seven_type = 1; $triad_type = ''; } elsif($triad_type eq 'minor') { ($third, $fifth) = (3, 7); $seven_type = 0; } elsif($triad_type eq 'dim') { ($third, $fifth) = (3, 6); $seven_type = -1; } elsif($triad_type eq 'aug') { ($third, $fifth) = (4, 8); $seven_type = 0; } elsif($triad_type eq 'adim') { ($third, $fifth) = (3, 6); $seven_type = 0; $seventh = 10; } my $number = extract_number($c->{"degree(?)"}->[0]->{number}); my $nummod = extract_mod($c->{"degree(?)"}->[0]->{"modifier(?)"}->[0]); my $alter = 0; if($nummod eq '#') { $alter = 1; } elsif($nummod eq 'b') { $alter = -1; } if($c->{"degree(?)"}->[0]->{major}) { $seven_type = 1; } if($number) { if($number > 15 || $number < 2) { die "Invalid degree $number (must be >= 2 and <= 15)\n"; } if($number >= 7) { $seventh = 10 + $seven_type; for(my $n = 9; $n < $number; $n += 2) { warn "\$n == $n" if $::VERBOSE; push @ext_notes, $notevals{$n % 7}; } warn "$number $alter " . $notevals{$number} if $::VERBOSE; if($number > 7) { push @ext_notes, $notevals{$number} + $alter; } } elsif($number == 5) { if($alter != 0) { $fifth += $alter; } else { $third = undef; } } elsif($number == 6) { push @ext_notes, $notevals{6 + $alter}; } elsif($number) { die "don't know what to do with number $number, try " . ($number + 7) . " or add" . ($number + 7) . " instead?"; } } my $sus_type = get_sus_type($c->{"sus(?)"}->[0]); # if($sus_type && ($triad_type ne '')) { # die "invalid: can't have both $triad_type and sus$sus_type third!\n"; # } warn "\$sus_type == $sus_type" if $::VERBOSE; if($sus_type == 2) { $third = 2; } elsif($sus_type == 4) { $third = 5; } for my $add (@{$c->{"add(s?)"}}) { my $addnum = extract_number($add->{number}); die "Invalid add '$addnum'" unless $addnum =~ /^\d+$/; die "Add '$addnum' out of range (must be >= 2 and <= 15)\n" unless $addnum >= 2 && $addnum <= 15; my $mod = extract_mod($add->{modifier}); my $addnote = mknote($addnum, $mod); if($addnum == 5) { $fifth = $addnote; } else { push @ext_notes, $addnote; } warn "add $mod $addnum ($addnote)\n" if $::VERBOSE; } for my $omit (@{$c->{"omit(s?)"}}) { my $omitnote = $omit->{omitval}->{__PATTERN1__}; if($omitnote == 3) { $third = undef; } elsif($omitnote == 5) { $fifth = undef; } else { warn "Don't yet know how to omit $omitnote, ignoring\n"; } } warn "triad type is '$triad_type', 3rd $third, 5th $fifth, 7th type $seven_type" if $::VERBOSE; if(defined $seventh) { warn "7th is $seventh\n" if $::VERBOSE; } if($::VERBOSE) { warn "extended note: $_\n" for @ext_notes; } $third += $note if defined $third; $fifth += $note if defined $fifth; $seventh += $note if defined $seventh; for(@ext_notes) { $_ += $note if defined $_; } my @output = (); push @output, get_note_name($bass) unless $bass == $note; for($note, $third, $fifth, $seventh, @ext_notes) { push @output, get_note_name($_) if defined $_; } print join " ", @output, "\n"; print "Music::Chord::Namer gives: " . chordname(@output) . "\n"; 1; } $::RD_ERRORS = $::RD_WARN = $::RD_HINT = $::VERBOSE; #$RD_TRACE = 1; #$RD_ERRORS = $RD_WARN = $RD_TRACE = 1; #$RD_AUTOACTION = q { print join ' ', @item, "\n" }; #$RD_AUTOACTION = q { main::print_array(@item); print "\n" }; $::RD_AUTOACTION = q { \%item }; our $parser = Parse::RecDescent->new($grammar); push @ARGV, "C#m/E" unless @ARGV; for(@ARGV) { my $arg = $_; # Allow major, Major, MAJOR, mAj, etc. $arg =~ s/min(or)?/min/i; $arg =~ s/maj(or)?/maj/i; # Allow root note name to be lowercase $arg =~ s/^([a-g])/uc $1/e; # Allow bass note name to be lowercase $arg =~ s/(\/)([a-g][#b]?)$/$1 . ucfirst $2/e; # Turn (foo) into /foo, allows for E7(#9) => E7/#9 $arg =~ s/\((.*?)\)/\/$1/g; # Allow some English: $arg =~ s/flat/b/gi; $arg =~ s/sharp/#/gi; $arg =~ s/augmented/aug/gi; $arg =~ s/diminished/dim/gi; $arg =~ s/(half|auto).?dim/adim/i; $arg =~ s/hendrix/7#9/i; my $fixedarg = $arg; my $chord = $parser->chord(\$arg) || die "Invalid chord: $_\n"; die "Trailing junk: $arg" if $arg; if($fixedarg eq $_) { print "$_: "; } else { print "$_ ($fixedarg): "; } create_chord($chord); my $cn = Music::Chord::Note->new(); print "Music::Chord::Note gives: "; eval { my @got = $cn->chord($fixedarg); print join " ", @got, "\n"; }; print $@ if $@; }