File Coverage

blib/lib/Music/PitchNum.pm
Criterion Covered Total %
statement 55 57 96.4
branch 32 34 94.1
condition 25 29 86.2
subroutine 7 7 100.0
pod 2 2 100.0
total 121 129 93.8


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # Musical note name and pitch number utility roles, mostly motivated by not
4             # wanting to drag in my huge and crufty Music::LilyPondUtil module just to
5             # figure out what pitch number fis'' is, and providing for such as a Role. Also
6             # an excuse for me to learn more about Roles as part of a Moo rewrite of
7             # various modules.
8             #
9             # Run perldoc(1) on this file for additional documentation.
10              
11             package Music::PitchNum;
12              
13 2     2   25001 use 5.010000;
  2         8  
  2         99  
14 2     2   484 use Moo::Role;
  2         17049  
  2         11  
15 2     2   1612 use POSIX qw/floor/;
  2         10991  
  2         12  
16 2     2   1754 use Scalar::Util qw/looks_like_number/;
  2         3  
  2         734  
17              
18             our $VERSION = '0.06';
19              
20             # for pitchnum (TODO make these attributes or otherwise f(x) calls?)
21             my %NOTE2NUM = (
22             C => 0,
23             D => 2,
24             E => 4,
25             F => 5,
26             G => 7,
27             A => 9,
28             B => 11,
29             );
30             # ASPN-style note-fu for pitchname
31             my %NUM2NOTE = (
32             0 => 'C',
33             1 => 'C#',
34             2 => 'D',
35             3 => 'D#',
36             4 => 'E',
37             5 => 'F',
38             6 => 'F#',
39             7 => 'G',
40             8 => 'G#',
41             9 => 'A',
42             10 => 'A#',
43             11 => 'B',
44             );
45              
46             ##############################################################################
47             #
48             # METHODS
49              
50             sub pitchname {
51 13     13 1 11902 my ( $self, $number ) = @_;
52 13 100       51 die "need a number for pitchname\n" if !looks_like_number $number;
53              
54 12         102 return $NUM2NOTE{ $number % 12 } . ( floor( $number / 12 ) - 1 );
55             }
56              
57             sub pitchnum {
58 35     35 1 62 my ( $self, $name ) = @_;
59              
60             # already a pitch number, but nix the decimal foo
61 35 100       134 return int $name if looks_like_number $name;
62              
63 33         25 my ( $octave, $note, $chrome );
64 129 100 66     496 SIRLEXALOT: {
      100        
      66        
65 33         28 last SIRLEXALOT
66             if $name =~ m/\G \z /cgx
67             or ( defined $octave and defined $note and defined $chrome );
68              
69             # Leading ,C as allowed by Helmholtz
70 96 50 66     326 if ( !defined $octave and $name =~ m/ \G ( [,]{1,10} ) /cgx ) {
71 0         0 $octave = $1;
72 0         0 redo SIRLEXALOT;
73             }
74              
75             # Simple note name support; insensitive, so supporting ABC notation not
76             # possible here; see ::German for H support, as this just matches A through
77             # G, and does not match solfege or the like.
78 96 100 100     295 if ( !defined $note
79             and $name =~
80             m/ \G (?: (?[A-G])(?\k{1,10}) | (?[A-Ga-g])) /cgx ) {
81 2     2   945 $note = $NOTE2NUM{ uc $+{note} };
  2         751  
  2         789  
  32         248  
82             # Optional "English multiple C notation" where C, is written CC (only for
83             # upper case as need to sometimes match "f" as "flat" as an accidental).
84 32 100 66     158 if ( defined $+{multi} and !defined $octave ) {
85 3         19 $octave = (',') x length $+{multi};
86             }
87 32         58 redo SIRLEXALOT;
88             }
89              
90 64 100 100     178 if ( defined $note
91             and !defined $octave ) {
92              
93 48 100       122 if ( $name =~ m/ \G ( [+-]?[0-9]{1,2} ) /cgx ) {
94             # ASPN octave number (hard: C4 is in no way relative to anything)
95 15         25 $octave = $1;
96 15         13 redo SIRLEXALOT;
97             }
98 33 100       73 if ( $name =~ m/ \G ( [,']{1,10} ) /cgx ) {
99             # Post-note a''' or b,, octave indications (soft; might be relative
100             # to something).
101 10         20 $octave = $1;
102 10         15 redo SIRLEXALOT;
103             }
104             }
105              
106             # Accidental (NOTE there is no microtonal support, e.g. lilypond beh);
107             # flat, sharp, doubleflat, doublesharp in various forms, mostly taken from
108             # MIDI::Simple and the "Note names in other languages" section of the
109             # lilypond notation documentation.
110 39 100 100     111 if ( defined $note and !defined $chrome ) {
111 27         27 my @howmany;
112 27 100       158 if ( @howmany = $name =~ m/ \G (ess|es|flat|[bf]) /cgx ) {
    100          
113 9         12 $chrome = -1 * @howmany;
114 9         14 redo SIRLEXALOT;
115             } elsif ( @howmany = $name =~ m/ \G (iss|is|sharp|[#dks]) /cgx ) {
116 10         11 $chrome = @howmany;
117 10         16 redo SIRLEXALOT;
118             }
119             }
120              
121             # nothing matched; nom something and try again at new position
122 20 50       47 if ( $name =~ m/ \G (?: \s+ | . ) /cgsx ) {
123 20         19 redo SIRLEXALOT;
124             }
125             }
126 33 100       60 return if !defined $note;
127              
128 32         21 my $pitchnum;
129              
130 32 100 100     124 if ( defined $octave and looks_like_number $octave ) {
131             # "hard" octave
132 15         34 $pitchnum = int( 12 * $octave + 12 + $note );
133              
134             } else {
135             # calculate the "hard" octave given the context...
136              
137 17   100     32 $octave //= ''; # equivalent to C3 under ASPN, if not relative
138 17 100       39 my $sign = ( $octave =~ m/,/ ) ? -1 : 1;
139 17         29 $octave = int( $sign * length $octave );
140              
141             # TODO support $relative as additional argument, but that's extra
142             # complication (and too much work for just getting the module out the
143             # door), as then must deal with which direction the tritone goes (see
144             # Music::LilyPondUtil).
145             #if ( defined $relative ) {
146             # ...
147 17         26 $pitchnum = int( 12 * $octave + 48 + $note );
148             }
149              
150 32 100       55 $pitchnum += $chrome if defined $chrome;
151              
152 32         147 return $pitchnum;
153             }
154              
155             1;
156             __END__