File Coverage

blib/lib/Music/PitchNum.pm
Criterion Covered Total %
statement 55 57 96.4
branch 34 36 94.4
condition 27 31 87.1
subroutine 7 7 100.0
pod 2 2 100.0
total 125 133 93.9


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