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