File Coverage

blib/lib/Music/LilyPondUtil.pm
Criterion Covered Total %
statement 199 214 92.9
branch 122 160 76.2
condition 38 50 76.0
subroutine 27 29 93.1
pod 16 16 100.0
total 402 469 85.7


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # http://www.lilypond.org/ related utility code (mostly to transition
4             # between Perl processing integers and the related appropriate letter
5             # names for the black dots in lilypond). See also Music::PitchNum.
6              
7             package Music::LilyPondUtil;
8              
9 1     1   22096 use 5.010000;
  1         3  
  1         38  
10 1     1   5 use strict;
  1         2  
  1         38  
11 1     1   4 use warnings;
  1         6  
  1         34  
12 1     1   4 use Carp qw/croak/;
  1         1  
  1         63  
13 1     1   4 use Scalar::Util qw/blessed looks_like_number/;
  1         1  
  1         86  
14 1     1   619 use Try::Tiny;
  1         1222  
  1         2496  
15              
16             our $VERSION = '0.55';
17              
18             # Since dealing with lilypond, assume 12 pitch material
19             my $DEG_IN_SCALE = 12;
20             my $TRITONE = 6;
21              
22             # Default register - due to "c" in lilypond absolute notation mapping to
23             # the fourth register, or MIDI pitch number 48. Used by the reg_*
24             # utility subs.
25             my $REL_DEF_REG = 4;
26              
27             # Just the note and register information - the 0,6 bit grants perhaps
28             # too much leeway for relative motion (silly things like c,,,,,,,
29             # relative to the top note on a piano) but there are other bounds on the
30             # results so that they lie within the span of the MIDI note numbers.
31             my $LY_NOTE_RE = qr/(([a-g])(?:eses|isis|es|is)?)(([,'])\g{-1}{0,6})?/;
32              
33             my %N2P = (
34             qw/bis 0 c 0 deses 0 bisis 1 cis 1 des 1 cisis 2 d 2 eeses 2 dis 3 ees 3 feses 3 disis 4 e 4 fes 4 eis 5 f 5 geses 5 eisis 6 fis 6 ges 6 fisis 7 g 7 aeses 7 gis 8 aes 8 gisis 9 a 9 beses 9 ais 10 bes 10 ceses 10 aisis 11 b 11 ces 11/
35             );
36             # mixing flats and sharps not supported, either one or other right now
37             my %P2N = (
38             flats => {qw/0 c 1 des 2 d 3 ees 4 e 5 f 6 ges 7 g 8 aes 9 a 10 bes 11 b/},
39             sharps => {qw/0 c 1 cis 2 d 3 dis 4 e 5 f 6 fis 7 g 8 gis 9 a 10 ais 11 b/},
40             );
41              
42             # Diabolus in Musica, indeed (direction tritone heads in relative mode)
43             my %TTDIR = (
44             flats => {qw/0 -1 1 1 2 -1 3 1 4 -1 5 1 6 1 7 -1 8 1 9 -1 10 1 11 -1/},
45             sharps => {qw/0 1 1 -1 2 1 3 -1 4 1 5 1 6 -1 7 1 8 -1 9 1 10 -1 11 -1/},
46             );
47              
48             ########################################################################
49             #
50             # SUBROUTINES
51              
52             sub _range_check {
53 330     330   313 my ( $self, $pitch ) = @_;
54 330 100       837 if ( $pitch < $self->{_min_pitch} ) {
    100          
55 3 100       6 if ( exists $self->{_min_pitch_hook} ) {
56 1         5 return $self->{_min_pitch_hook}
57             ( $pitch, $self->{_min_pitch}, $self->{_max_pitch}, $self );
58             } else {
59 2         10 die "pitch $pitch is too low\n";
60             }
61              
62             } elsif ( $pitch > $self->{_max_pitch} ) {
63 3 100       9 if ( exists $self->{_max_pitch_hook} ) {
64 1         4 return $self->{_max_pitch_hook}
65             ( $pitch, $self->{_min_pitch}, $self->{_max_pitch}, $self );
66             } else {
67 2         9 die "pitch $pitch is too high\n";
68             }
69             }
70              
71 324         528 return;
72             }
73              
74             sub _symbol2relreg {
75 107     107   92 my ($symbol) = @_;
76 107   100     203 $symbol ||= q{};
77              
78             # no leap, within three stave lines of previous note
79 107 100       176 return 0 if length $symbol == 0;
80              
81 51 50       143 die "invalid register symbol $symbol\n"
82             if $symbol !~ m/^(([,'])\g{-1}*)$/;
83              
84 51         50 my $count = length $1;
85 51 100       74 $count *= $2 eq q{'} ? 1 : -1;
86              
87 51         56 return $count;
88             }
89              
90             sub chrome {
91 5     5 1 14 my ( $self, $chrome ) = @_;
92 5 100       13 if ( defined $chrome ) {
93 4 50       10 croak q{chrome must be 'sharps' or 'flats'} unless exists $P2N{$chrome};
94 4         7 $self->{_chrome} = $chrome;
95             }
96 5         14 return $self->{_chrome};
97             }
98              
99             sub clear_prev_note {
100 0     0 1 0 my ($self) = @_;
101 0         0 undef $self->{prev_note};
102             }
103              
104             sub clear_prev_pitch {
105 1     1 1 2 my ($self) = @_;
106 1         3 undef $self->{prev_pitch};
107             }
108              
109             # diatonic (piano white key) pitch number for a given input note (like
110             # prev_note() below except without side-effects).
111             sub diatonic_pitch {
112 3     3 1 448 my ( $self, $note ) = @_;
113              
114 3 50       9 croak 'note not defined' unless defined $note;
115              
116 3         3 my $pitch;
117 3 50       75 if ( $note =~ m/^$LY_NOTE_RE/ ) {
118             # TODO duplicates (portions of) same code, below
119 3         6 my $real_note = $1;
120 3         4 my $diatonic_note = $2;
121 3   50     8 my $reg_symbol = $3 // '';
122              
123 3 50       16 croak "unknown lilypond note $note" unless exists $N2P{$real_note};
124              
125 3         11 $pitch =
126             $N2P{$diatonic_note} + $self->reg_sym2num($reg_symbol) * $DEG_IN_SCALE;
127 3 50       13 $pitch %= $DEG_IN_SCALE if $self->{_ignore_register};
128              
129             } else {
130 0         0 croak "unknown note $note";
131             }
132              
133 3         11 return $pitch;
134             }
135              
136             sub ignore_register {
137 2     2 1 10 my ( $self, $state ) = @_;
138 2 100       6 $self->{_ignore_register} = $state if defined $state;
139 2         7 return $self->{_ignore_register};
140             }
141              
142             sub keep_state {
143 3     3 1 9 my ( $self, $state ) = @_;
144 3 100       9 $self->{_keep_state} = $state if defined $state;
145 3         7 return $self->{_keep_state};
146             }
147              
148             sub mode {
149 5     5 1 15 my ( $self, $mode ) = @_;
150 5 100       12 if ( defined $mode ) {
151 4 50 66     19 croak q{mode must be 'absolute' or 'relative'}
152             if $mode ne 'absolute' and $mode ne 'relative';
153 4         32 $self->{_mode} = $mode;
154             }
155 5         15 return $self->{_mode};
156             }
157              
158             sub new {
159 11     11 1 946 my ( $class, %param ) = @_;
160 11         19 my $self = {};
161              
162 11   100     57 $self->{_chrome} = $param{chrome} || 'sharps';
163 11 50       29 croak q{chrome must be 'sharps' or 'flats'}
164             unless exists $P2N{ $self->{_chrome} };
165              
166 11   100     41 $self->{_keep_state} = $param{keep_state} // 1;
167 11   100     38 $self->{_ignore_register} = $param{ignore_register} // 0;
168              
169             # Default min_pitch of 21 causes too many problems for existing code,
170             # so minimum defaults to 0, which is a bit beyond the bottom of 88-key
171             # pianos. 108 is the top of a standard 88-key piano.
172 11   100     35 $self->{_min_pitch} = $param{min_pitch} // 0;
173 11   100     36 $self->{_max_pitch} = $param{max_pitch} // 108;
174              
175 11 100       20 if ( exists $param{min_pitch_hook} ) {
176 1 50       3 croak 'min_pitch_hook must be code ref'
177             unless ref $param{min_pitch_hook} eq 'CODE';
178 1         3 $self->{_min_pitch_hook} = $param{min_pitch_hook};
179             }
180 11 100       24 if ( exists $param{max_pitch_hook} ) {
181 1 50       4 croak 'max_pitch_hook must be code ref'
182             unless ref $param{max_pitch_hook} eq 'CODE';
183 1         2 $self->{_max_pitch_hook} = $param{max_pitch_hook};
184             }
185              
186 11   100     32 $self->{_mode} = $param{mode} || 'absolute';
187 11 50 66     34 croak q{'mode' must be 'absolute' or 'relative'}
188             if $self->{_mode} ne 'absolute' and $self->{_mode} ne 'relative';
189              
190             $self->{_p2n_hook} = $param{p2n_hook}
191 11   50 324   68 || sub { $P2N{ $_[1] }->{ $_[0] % $DEG_IN_SCALE } };
  324         637  
192 11 50       26 croak q{'p2n_hook' must be code ref}
193             unless ref $self->{_p2n_hook} eq 'CODE';
194              
195 11   100     43 $self->{_sticky_state} = $param{sticky_state} // 0;
196 11   100     32 $self->{_strip_rests} = $param{strip_rests} // 0;
197              
198 11         26 bless $self, $class;
199 11         28 return $self;
200             }
201              
202             sub notes2pitches {
203 15     15 1 52 my $self = shift;
204 15         14 my @pitches;
205              
206 15 50       45 for my $n ( ref $_[0] eq 'ARRAY' ? @{ $_[0] } : @_ ) {
  0         0  
207             # pass through what hopefully are raw pitch numbers, otherwise parse
208             # note from subset of the lilypond note format
209 144 50 66     1175 if ( !defined $n ) {
    100          
    100          
    100          
210             # might instead blow up? or have option to blow up...
211 0 0       0 push @pitches, undef unless $self->{_strip_rests};
212              
213             } elsif ( $n =~ m/^(-?\d+)$/ ) {
214 3         5 push @pitches, $n;
215              
216             } elsif ( $n =~ m/^(?i)[rs]/ or $n =~ m/\\rest/ ) {
217             # rests or lilypond 'silent' bits
218 3 100       9 push @pitches, undef unless $self->{_strip_rests};
219              
220             } elsif ( $n =~ m/^$LY_NOTE_RE/ ) {
221             # "diatonic" (here, the white notes of a piano) are necessary
222             # for leap calculations in relative mode, as "cisis" goes down
223             # to "aeses" despite the real notes ("d" and "g," in absolute
224             # mode) being a fifth apart. Another way to think of it: the
225             # diatonic "c" and "a" of "cisis" and "aeses" are within three
226             # stave lines of one another; anything involving three or more
227             # stave lines is a leap.
228 137         162 my $real_note = $1;
229 137         121 my $diatonic_note = $2;
230 137   100     338 my $reg_symbol = $3 // '';
231              
232 137 50       193 croak "unknown lilypond note $n" unless exists $N2P{$real_note};
233              
234 137         98 my ( $diatonic_pitch, $real_pitch );
235 137 100       180 if ( $self->{_mode} ne 'relative' ) { # absolute
236             # TODO see if can do this code regardless of mode, and still
237             # sanity check the register for absolute/relative-no-previous,
238             # but not for relative-with-previous, to avoid code
239             # duplication in abs/r-no-p blocks - or call subs with
240             # appropriate register numbers.
241 56         73 ( $diatonic_pitch, $real_pitch ) =
242 28         31 map { $N2P{$_} + $self->reg_sym2num($reg_symbol) * $DEG_IN_SCALE }
243             $diatonic_note, $real_note;
244              
245             # Account for edge cases of ces and bis and the like
246 28         29 my $delta = $diatonic_pitch - $real_pitch;
247 28 100       49 if ( abs($delta) > $TRITONE ) {
248 3 100       6 $real_pitch += $delta > 0 ? $DEG_IN_SCALE : -$DEG_IN_SCALE;
249             }
250              
251             } else { # relatively more complicated
252              
253 109 100       120 if ( !defined $self->{prev_note} ) { # absolute if nothing prior
254 4         6 ( $diatonic_pitch, $real_pitch ) =
255 2         4 map { $N2P{$_} + $self->reg_sym2num($reg_symbol) * $DEG_IN_SCALE }
256             $diatonic_note, $real_note;
257              
258             # Account for edge cases of ces and bis and the like
259 2         4 my $delta = $diatonic_pitch - $real_pitch;
260 2 50       5 if ( abs($delta) > $TRITONE ) {
261 0 0       0 $real_pitch += $delta > 0 ? $DEG_IN_SCALE : -$DEG_IN_SCALE;
262             }
263              
264             } else { # meat of relativity
265 107         135 my $reg_number =
266             int( $self->{prev_note} / $DEG_IN_SCALE ) * $DEG_IN_SCALE;
267              
268 107         125 my $reg_delta =
269             $self->{prev_note} % $DEG_IN_SCALE - $N2P{$diatonic_note};
270 107 100       153 if ( abs($reg_delta) > $TRITONE ) {
271 31 100       37 $reg_number += $reg_delta > 0 ? $DEG_IN_SCALE : -$DEG_IN_SCALE;
272             }
273              
274             # adjust register by the required relative offset
275 107         141 my $reg_offset = _symbol2relreg($reg_symbol);
276 107 100       164 if ( $reg_offset != 0 ) {
277 51         55 $reg_number += $reg_offset * $DEG_IN_SCALE;
278             }
279              
280 214         279 ( $diatonic_pitch, $real_pitch ) =
281 107         94 map { $reg_number + $N2P{$_} } $diatonic_note, $real_note;
282              
283 107         97 my $delta = $diatonic_pitch - $real_pitch;
284 107 100       168 if ( abs($delta) > $TRITONE ) {
285 10 100       16 $real_pitch += $delta > 0 ? $DEG_IN_SCALE : -$DEG_IN_SCALE;
286             }
287             }
288              
289 109 50       184 $self->{prev_note} = $diatonic_pitch if $self->{_keep_state};
290             }
291              
292 137         192 push @pitches, $real_pitch;
293              
294             } else {
295 1         27 croak "unknown note '$n'";
296             }
297             }
298              
299 14 100       27 if ( $self->{_ignore_register} ) {
300 1         3 for my $p (@pitches) {
301 4 50       8 $p %= $DEG_IN_SCALE if defined $p;
302             }
303             }
304              
305 14 50       45 undef $self->{prev_note} unless $self->{_sticky_state};
306              
307 14 100       115 return @pitches > 1 ? @pitches : $pitches[0];
308             }
309              
310             # Converts pitches to lilypond names
311             sub p2ly {
312 36     36 1 1645 my $self = shift;
313              
314 36         39 my @notes;
315 36 50       104 for my $obj ( ref $_[0] eq 'ARRAY' ? @{ $_[0] } : @_ ) {
  0         0  
316 330         237 my $pitch;
317 330 50 33     1369 if ( !defined $obj ) {
    50          
    50          
318 0         0 croak "cannot convert undefined value to lilypond element";
319             } elsif ( blessed $obj and $obj->can("pitch") ) {
320 0         0 $pitch = $obj->pitch;
321             } elsif ( looks_like_number $obj) {
322 330         386 $pitch = int $obj;
323             } else {
324             # pass through on unknowns (could be rests or who knows what)
325 0         0 push @notes, $obj;
326 0         0 next;
327             }
328              
329             # Response handling on range check:
330             # * exception - out of bounds, default die() handler tripped
331             # * defined return value - got something from a hook function, use that
332             # * undefined - pitch is within bounds, continue with code below
333 330         224 my $range_result;
334 330     330   10024 try { $range_result = $self->_range_check($pitch) }
335             catch {
336 4     4   72 croak $_;
337 330         1432 };
338 326 100       2726 if ( defined $range_result ) {
339 2         3 push @notes, $range_result;
340 2         5 next;
341             }
342              
343 324         489 my $note = $self->{_p2n_hook}( $pitch, $self->{_chrome} );
344 324 50       563 croak "could not lookup note for pitch '$pitch'" unless defined $note;
345              
346 324         232 my $register;
347 324 100       444 if ( $self->{_mode} ne 'relative' ) {
348 20         37 $register = $self->reg_num2sym( $pitch / $DEG_IN_SCALE );
349              
350             } else { # relatively more complicated
351 304         244 my $rel_reg = $REL_DEF_REG;
352 304 100       501 if ( defined $self->{prev_pitch} ) {
353 290         338 my $delta = int( $pitch - $self->{prev_pitch} );
354 290 100       508 if ( abs($delta) >= $TRITONE ) { # leaps need , or ' variously
355 197 100       262 if ( $delta % $DEG_IN_SCALE == $TRITONE ) {
356 152         187 $rel_reg += int( $delta / $DEG_IN_SCALE );
357              
358             # Adjust for tricky changing tritone default direction
359 152         231 my $default_dir =
360             $TTDIR{ $self->{_chrome} }
361             ->{ $self->{prev_pitch} % $DEG_IN_SCALE };
362 152 100 100     708 if ( $delta > 0 and $default_dir < 0 ) {
    100 100        
363 36         47 $rel_reg++;
364             } elsif ( $delta < 0 and $default_dir > 0 ) {
365 36         45 $rel_reg--;
366             }
367              
368             } else { # not tritone, but leap
369             # TT adjust is to push <1 leaps out so become 1
370 45 100       324 $rel_reg +=
371             int( ( $delta + ( $delta > 0 ? $TRITONE : -$TRITONE ) ) /
372             $DEG_IN_SCALE );
373             }
374             }
375             }
376 304         389 $register = $self->reg_num2sym($rel_reg);
377 304 50       649 $self->{prev_pitch} = $pitch if $self->{_keep_state};
378             }
379              
380             # Do not care about register (even in absolute mode) if keeping state
381 324 100       404 if ( $self->{_keep_state} ) {
382 316 50       484 croak "register out of range for pitch '$pitch'"
383             unless defined $register;
384             } else {
385 8         7 $register = '';
386             }
387 324         593 push @notes, $note . $register;
388             }
389              
390 32 100       79 undef $self->{prev_pitch} unless $self->{_sticky_state};
391 32 100       378 return @_ > 1 ? @notes : $notes[0];
392             }
393              
394             # MUST NOT accept raw pitch numbers, as who knows if "61" is a "cis"
395             # or "des" or the like, which will in turn affect the relative
396             # calculations!
397             sub prev_note {
398 2     2 1 4 my ( $self, $pitch ) = @_;
399 2 50       5 if ( defined $pitch ) {
400 2 50       66 if ( $pitch =~ m/^$LY_NOTE_RE/ ) {
401             # TODO duplicates (portions of) same code, below
402 2         5 my $real_note = $1;
403 2         3 my $diatonic_note = $2;
404 2   50     6 my $reg_symbol = $3 // '';
405              
406 2 50       5 croak "unknown lilypond note $pitch" unless exists $N2P{$real_note};
407              
408             # for relative-to-this just need the diatonic
409 2         4 $self->{prev_note} =
410             $N2P{$diatonic_note} +
411             $self->reg_sym2num($reg_symbol) * $DEG_IN_SCALE;
412              
413             } else {
414 0         0 croak "unknown pitch '$pitch'";
415             }
416             }
417 2         10 return $self->{prev_note};
418             }
419              
420             sub prev_pitch {
421 3     3 1 427 my ( $self, $pitch ) = @_;
422 3 100       8 if ( defined $pitch ) {
423 1 50 33     11 if ( blessed $pitch and $pitch->can("pitch") ) {
    50          
424 0         0 $self->{prev_pitch} = $pitch->pitch;
425             } elsif ( looks_like_number $pitch) {
426 0         0 $self->{prev_pitch} = int $pitch;
427             } else {
428 1     1   34 try { $self->{prev_pitch} = $self->diatonic_pitch($pitch) }
429             catch {
430 0     0   0 croak $_;
431 1         7 };
432             }
433             }
434 3         20 return $self->{prev_pitch};
435             }
436              
437             # Utility, converts arbitrary numbers into lilypond register notation
438             sub reg_num2sym {
439 327     327 1 452 my ( $self, $number ) = @_;
440 327 50 33     1093 croak 'register number must be numeric'
441             if !defined $number
442             or !looks_like_number $number;
443              
444 327         280 $number = int $number;
445 327         315 my $symbol = q{};
446 327 100       1325 if ( $number < $REL_DEF_REG ) {
    100          
447 77         100 $symbol = q{,} x ( $REL_DEF_REG - $number );
448             } elsif ( $number > $REL_DEF_REG ) {
449 77         108 $symbol = q{'} x ( $number - $REL_DEF_REG );
450             }
451 327         472 return $symbol;
452             }
453              
454             # Utility, converts arbitrary ,, or ''' into appropriate register number
455             sub reg_sym2num {
456 68     68 1 74 my ( $self, $symbol ) = @_;
457 68 50       100 croak 'undefined register symbol' unless defined $symbol;
458 68 50       169 croak 'invalid register symbol' unless $symbol =~ m/^(,|')*$/;
459              
460 68 100       88 my $dir = $symbol =~ m/[,]/ ? -1 : 1;
461              
462 68         165 return $REL_DEF_REG + $dir * length $symbol;
463             }
464              
465             sub sticky_state {
466 3     3 1 10 my ( $self, $state ) = @_;
467 3 100       8 $self->{_sticky_state} = $state if defined $state;
468 3         7 return $self->{_sticky_state};
469             }
470              
471             sub strip_rests {
472 2     2 1 10 my ( $self, $state ) = @_;
473 2 100       6 $self->{_strip_rests} = $state if defined $state;
474 2         5 return $self->{_strip_rests};
475             }
476              
477             1;
478             __END__