File Coverage

blib/lib/Music/PitchNum.pm
Criterion Covered Total %
statement 54 56 96.4
branch 34 36 94.4
condition 25 29 86.2
subroutine 7 7 100.0
pod 2 2 100.0
total 122 130 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
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   56767 use 5.010000;
  2         4  
14 2     2   399 use Moo::Role;
  2         13245  
  2         9  
15 2     2   1297 use POSIX qw/floor/;
  2         8346  
  2         8  
16 2     2   1629 use Scalar::Util qw/looks_like_number/;
  2         2  
  2         634  
17              
18             our $VERSION = '0.08';
19              
20             ##############################################################################
21             #
22             # ATTRIBUTES
23             #
24             # TODO this code is (sometimes) duplicated in the other modules in this
25             # distribution, might be nice to "extend" this one into the other roles
26             # and override things as necessary, but it's not clear how or whether
27             # that can be done in the Moo::Role docs.
28              
29             # These probably should be 'ro' but there could be cases where someone
30             # wants to set their own note names, so...
31             has NOTE2NUM => (
32             is => 'rw',
33             default => sub {
34             { C => 0,
35             D => 2,
36             E => 4,
37             F => 5,
38             G => 7,
39             A => 9,
40             B => 11,
41             };
42             },
43             );
44              
45             has NUM2NOTE => (
46             is => 'rw',
47             # ASPN-style by default
48             default => sub {
49             { 0 => 'C',
50             1 => 'C#',
51             2 => 'D',
52             3 => 'D#',
53             4 => 'E',
54             5 => 'F',
55             6 => 'F#',
56             7 => 'G',
57             8 => 'G#',
58             9 => 'A',
59             10 => 'A#',
60             11 => 'B',
61             };
62             },
63             );
64              
65             # Option to omit the octave info, for when you need just a plain "C#"
66             # from pitchname and not "C#4" or worse "C#-1" if you're dealing with
67             # integers in the 0..11 range.
68             #
69             # TODO this probably should be a "Bool" type
70             has ignore_octave => (
71             is => 'rw',
72             coerce => sub {
73             $_[0] ? 1 : 0;
74             },
75             default => 0,
76             );
77              
78             ##############################################################################
79             #
80             # METHODS
81              
82             sub pitchname {
83 15     15 1 50 my ( $self, $number ) = @_;
84 15 100       47 die "need a number for pitchname\n" if !looks_like_number $number;
85              
86 14 100       50 return $self->NUM2NOTE->{ $number % 12 }
87             . ( $self->ignore_octave ? '' : ( floor( $number / 12 ) - 1 ) );
88             }
89              
90             sub pitchnum {
91 35     35 1 49 my ( $self, $name ) = @_;
92              
93             # already a pitch number, but nix the decimal foo
94 35 100       104 return int $name if looks_like_number $name;
95              
96 33         23 my ( $octave, $note, $chrome );
97             SIRLEXALOT: {
98 33         20 last SIRLEXALOT
99 129 100 66     359 if $name =~ m/\G \z /cgx
      100        
      66        
100             or ( defined $octave and defined $note and defined $chrome );
101              
102             # Leading ,C as allowed by Helmholtz
103 96 50 66     246 if ( !defined $octave and $name =~ m/ \G ( [,]{1,10} ) /cgx ) {
104 0         0 $octave = $1;
105 0         0 redo SIRLEXALOT;
106             }
107              
108             # Simple note name support; insensitive, so supporting ABC notation not
109             # possible here; see ::German for H support, as this just matches A through
110             # G, and does not match solfege or the like.
111 96 100 100     247 if ( !defined $note
112             and $name =~
113             m/ \G (?: (?[A-G])(?\k{1,10}) | (?[A-Ga-g])) /cgx ) {
114 2     2   785 $note = $self->NOTE2NUM->{ uc $+{note} };
  2         559  
  2         629  
  32         172  
115             # Optional "English multiple C notation" where C, is written CC (only for
116             # upper case as need to sometimes match "f" as "flat" as an accidental).
117 32 100 66     107 if ( defined $+{multi} and !defined $octave ) {
118 3         10 $octave = (',') x length $+{multi};
119             }
120 32         39 redo SIRLEXALOT;
121             }
122              
123 64 100 100     143 if ( defined $note
124             and !defined $octave ) {
125              
126 48 100       88 if ( $name =~ m/ \G ( [+-]?[0-9]{1,2} ) /cgx ) {
127             # ASPN octave number (hard: C4 is in no way relative to anything)
128 15         20 $octave = $1;
129 15         13 redo SIRLEXALOT;
130             }
131 33 100       50 if ( $name =~ m/ \G ( [,']{1,10} ) /cgx ) {
132             # Post-note a''' or b,, octave indications (soft; might be relative
133             # to something).
134 10         14 $octave = $1;
135 10         8 redo SIRLEXALOT;
136             }
137             }
138              
139             # Accidental (NOTE there is no microtonal support, e.g. lilypond beh);
140             # flat, sharp, doubleflat, doublesharp in various forms, mostly taken from
141             # MIDI::Simple and the "Note names in other languages" section of the
142             # lilypond notation documentation.
143 39 100 100     85 if ( defined $note and !defined $chrome ) {
144 27         16 my @howmany;
145 27 100       110 if ( @howmany = $name =~ m/ \G (ess|es|flat|[bf]) /cgx ) {
    100          
146 9         9 $chrome = -1 * @howmany;
147 9         10 redo SIRLEXALOT;
148             } elsif ( @howmany = $name =~ m/ \G (iss|is|sharp|[#dks]) /cgx ) {
149 10         6 $chrome = @howmany;
150 10         14 redo SIRLEXALOT;
151             }
152             }
153              
154             # nothing matched; nom something and try again at new position
155 20 50       36 if ( $name =~ m/ \G (?: \s+ | . ) /cgsx ) {
156 20         15 redo SIRLEXALOT;
157             }
158             }
159 33 100       45 return if !defined $note;
160              
161 32         20 my $pitchnum;
162              
163 32 100 100     100 if ( defined $octave and looks_like_number $octave ) {
164             # "hard" octave
165 15         21 $pitchnum = int( 12 * $octave + 12 + $note );
166              
167             } else {
168             # calculate the "hard" octave given the context...
169              
170 17   100     27 $octave //= ''; # equivalent to C3 under ASPN, if not relative
171 17 100       22 my $sign = ( $octave =~ m/,/ ) ? -1 : 1;
172 17         16 $octave = int( $sign * length $octave );
173              
174             # TODO support $relative as additional argument, but that's extra
175             # complication (and too much work for just getting the module out the
176             # door), as then must deal with which direction the tritone goes (see
177             # Music::LilyPondUtil).
178             #if ( defined $relative ) {
179             # ...
180 17         19 $pitchnum = int( 12 * $octave + 48 + $note );
181             }
182              
183 32 100       39 $pitchnum += $chrome if defined $chrome;
184              
185 32         113 return $pitchnum;
186             }
187              
188             1;
189             __END__