File Coverage

blib/lib/Music/RhythmSet/Voice.pm
Criterion Covered Total %
statement 203 203 100.0
branch 90 90 100.0
condition 101 101 100.0
subroutine 15 15 100.0
pod 7 7 100.0
total 416 416 100.0


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # a voice (or track) that is comprised of various patterns repeated
4             # ttl times
5              
6             package Music::RhythmSet::Voice;
7             our $VERSION = '0.06';
8              
9 3     3   495525 use 5.24.0;
  3         12  
10 3     3   10 use warnings;
  3         8  
  3         146  
11 3     3   11 use Carp qw(confess croak);
  3         5  
  3         130  
12 3     3   1194 use MIDI;
  3         26813  
  3         127  
13 3     3   1055 use Moo;
  3         20439  
  3         14  
14 3     3   5119 use namespace::clean;
  3         43397  
  3         22  
15              
16 3     3   931 use constant { NOTE_ON => 1, NOTE_OFF => 0, EVENT => 0, DTIME => 1 };
  3         11  
  3         10399  
17              
18             has id => ( is => 'rw' );
19             has next => ( is => 'rw' );
20             has measure => ( is => 'rw', default => sub { 0 } );
21             has pattern => ( is => 'rw' );
22             has replay => ( is => 'rw', default => sub { [] } );
23             has stash => ( is => 'rw' );
24             has ttl => ( is => 'rw', default => sub { 0 } );
25              
26             # perldoc Moo
27             sub BUILD {
28 46     46 1 177 my ( $self, $args ) = @_;
29 46 100 100     227 if ( exists $args->{pattern} and exists $args->{ttl} ) {
30 9 100       288 croak "invalid ttl" if $args->{ttl} < 1;
31             croak "invalid pattern"
32             unless defined $args->{pattern}
33             and ref $args->{pattern} eq 'ARRAY'
34 7 100 100     369 and $args->{pattern}->@*;
      100        
35 4         34 push $self->replay->@*, [ $args->{pattern}, $args->{ttl} ];
36             }
37             }
38              
39             ########################################################################
40             #
41             # METHODS
42              
43             sub advance {
44 28     28 1 5312 my ( $self, $count, %param ) = @_;
45              
46 28         52 my $measure = $self->measure;
47              
48 28   100     68 for ( 1 .. $count // 1 ) {
49 36         62 my $ttl = $self->ttl - 1;
50              
51 36         50 $param{measure} = $measure++;
52 36         52 $param{pattern} = $self->pattern;
53              
54 36 100       54 if ( $ttl <= 0 ) {
55 22         74 my $next = $self->next;
56              
57 22 100 100     451 confess "no next callback"
58             unless defined $next and ref $next eq 'CODE';
59              
60 20         48 ( $param{pattern}, $ttl ) = $next->( $self, %param );
61              
62             confess "no pattern set"
63             unless defined $param{pattern}
64             and ref $param{pattern} eq 'ARRAY'
65 20 100 100     777 and $param{pattern}->@*;
      100        
66 17 100       162 confess "invalid ttl" if $ttl < 1;
67              
68 16         49 $self->pattern( $param{pattern} );
69              
70 16         44 push $self->replay->@*, [ $param{pattern}, $ttl ];
71             }
72              
73 30         75 $self->ttl($ttl);
74             }
75              
76 22         35 $self->measure($measure);
77              
78 22         51 return $self;
79             }
80              
81             # there is no ->changes method; meanwhile, put the single voice into a
82             # set object and call ->changes over there if you need that for a
83             # single voice:
84             #
85             # my $set = Music::RhythmSet->new;
86             # $set->voices([$voice]);
87             # $set->changes(...)
88              
89             sub clone {
90 10     10 1 1438 my ( $self, %param ) = @_;
91              
92 10   100     56 $param{newid} //= $self->id;
93              
94             my $new = Music::RhythmSet::Voice->new(
95             id => $param{newid},
96 10         21 map { $_, scalar $self->$_ } qw(next measure ttl),
  30         239  
97             );
98              
99             # these 'die' as the bad attribute values were likely not assigned
100             # anywhere near the current stack. use Carp::Always or such if you
101             # do need to find out where your code calls into here, but you
102             # probably instead want to look at any ->pattern(...) or
103             # ->replay(...) calls in your code
104 10         23 my $pat = $self->pattern;
105 10 100       16 if ( defined $pat ) {
106 6 100 100     59 die "invalid pattern" unless ref $pat eq 'ARRAY' and $pat->@*;
107 4         12 $new->pattern( [ $pat->@* ] );
108             }
109              
110 8         35 my $ref = $self->replay;
111 8 100       46 if ( defined $ref ) {
112 7 100       26 die "replay must be an array reference"
113             unless ref $ref eq 'ARRAY';
114 6 100       23 die "replay array must contain array references"
115             unless ref $ref->[0] eq 'ARRAY';
116 5         11 $new->replay( [ map { [ [ $_->[0]->@* ], $_->[1] ] } $ref->@* ] );
  13         43  
117             }
118              
119 6         22 return $new;
120             }
121              
122             sub from_string {
123 11     11 1 1639 my ( $self, $str, %param ) = @_;
124 11 100 100     307 croak "need a string" unless defined $str and length $str;
125              
126 9   100     27 $param{rs} //= "\n";
127 9 100       15 if ( $param{sep} ) {
128 2         16 $param{sep} = qr/\Q$param{sep}\E/;
129             } else {
130 7         18 $param{sep} = qr/\s+/;
131             }
132              
133 9         10 my $linenum = 1;
134 9         21 my @newplay;
135              
136 9         58 for my $line ( split /\Q$param{rs}/, $str ) {
137 22 100       54 next if $line =~ m/^\s*(?:#|$)/;
138             # the limits are to prevent overly long strings from being
139             # parsed; if this is a problem write a modified from_string that
140             # does allow such inputs, or modify the unused count
141 18 100       292 if ($line =~ m/^
142             (?\d{1,10}) $param{sep}
143             (?.*?) $param{sep}
144             (?[x.]{1,256}) $param{sep}
145             (?\d{1,5}) \s*(?:[#].*)?
146             $/ax
147             ) {
148             # NOTE is unused and is assumed to be "this voice"
149             # regardless of what it contains
150 14         90 push @newplay, [ [ split //, $+{bstr} =~ tr/x./10/r ], $+{ttl} ];
151             } else {
152 4         402 croak "invalid record at line $linenum";
153             }
154 14         34 $linenum++;
155             }
156              
157 5         31 push $self->replay->@*, @newplay;
158              
159 5         25 return $self;
160             }
161              
162             # TODO some means of note reduction and optional note sustains
163             # over rests
164             sub to_ly {
165 10     10 1 1132 my ( $self, %param ) = @_;
166              
167 10         24 my $replay = $self->replay;
168 10 100 100     323 croak "empty replay log"
      100        
169             unless defined $replay
170             and ref $replay eq 'ARRAY'
171             and $replay->@*;
172              
173 7   100     29 $param{dur} //= '16';
174 7   100     23 $param{note} //= 'c';
175 7   100     38 $param{rest} //= 'r';
176              
177 7   100     19 my $id = $self->id // '';
178 7         13 my $ly = '';
179 7   100     26 my $maxm = $param{maxm} // ~0;
180              
181 7         15 for my $ref ( $replay->@* ) {
182 10         13 my ( $bpat, $ttl ) = $ref->@*;
183 10 100       23 $ttl = $maxm if $ttl > $maxm;
184              
185 10         39 $ly .= " % v$id " . join( '', $bpat->@* ) =~ tr/10/x./r . " $ttl\n";
186 10 100       21 if ( $param{time} ) {
187 2         3 $ly .= ' \time ' . $bpat->@* . '/' . $param{time} . "\n";
188             }
189 10         14 my $str = ' ';
190 10         13 for my $x ( $bpat->@* ) {
191 26 100       36 if ( $x == NOTE_ON ) {
192 16         23 $str .= ' ' . $param{note} . $param{dur};
193             } else {
194 10         27 $str .= ' ' . $param{rest} . $param{dur};
195             }
196             }
197 10         21 $ly .= join( "\n", ($str) x $ttl ) . "\n";
198              
199 10         15 $maxm -= $ttl;
200 10 100       20 last if $maxm <= 0;
201             }
202 7         36 return $ly;
203             }
204              
205             sub to_midi {
206 15     15 1 7591 my ( $self, %param ) = @_;
207              
208 15         28 my $replay = $self->replay;
209 15 100 100     316 croak "empty replay log"
      100        
210             unless defined $replay
211             and ref $replay eq 'ARRAY'
212             and $replay->@*;
213              
214             # MIDI::Event, section "EVENTS AND THEIR DATA TYPES"
215 12   100     41 $param{chan} //= 0;
216 12   100     28 $param{dur} //= 20;
217 12   100     30 $param{note} //= 60;
218 12   100     31 $param{tempo} //= 500_000;
219 12   100     53 $param{velo} //= 90; # "default value" per lilypond scm/midi.scm
220              
221 12         49 my $track = MIDI::Track->new;
222 12         293 my $events = $track->events_r;
223              
224 12         53 my $delay;
225 12   100     44 my $id = $self->id // '';
226 12         17 my $leftover = 0;
227 12   100     50 my $maxm = $param{maxm} // ~0;
228              
229 12 100       69 push $events->@*, [ 'track_name', 0, 'voice' . ( length $id ? " $id" : '' ) ];
230 12         22 push $events->@*, [ 'set_tempo', 0, $param{tempo} ];
231 12 100       60 if ( $param{patch_change} ) {
232 1         2 push $events->@*, [ 'patch_change', 0, $param{chan}, $param{patch_change} ];
233             }
234              
235 12         23 for my $ref ( $replay->@* ) {
236 19         34 my ( $bpat, $ttl ) = $ref->@*;
237 19 100       46 $ttl = $maxm if $ttl > $maxm;
238              
239 19         307 push $events->@*,
240             [ 'text_event', $leftover,
241             "v$id " . join( '', $bpat->@* ) =~ tr/10/x./r . " $ttl\n"
242             ];
243              
244 19         27 $delay = 0;
245 19         24 my ( $onsets, $open, @midi );
246              
247 19         24 for my $x ( $bpat->@* ) {
248 46 100       64 if ( $x == NOTE_ON ) {
249 26         30 $onsets++;
250 26 100       40 if ( defined $open ) {
251 11         19 push @midi, [ 'note_off', $delay, $param{chan}, $open, 0 ];
252 11         12 $delay = 0;
253             }
254 26         34 push @midi, [ 'note_on', $delay, map { $param{$_} } qw(chan note velo) ];
  78         114  
255 26         33 $delay = $param{dur};
256 26         33 $open = $param{note};
257             } else {
258 20 100       27 if ( defined $open ) {
259 7         22 push @midi, [ 'note_off', $delay, $param{chan}, $open, 0 ];
260 7         7 $delay = 0;
261 7         9 undef $open;
262             }
263 20         70 $delay += $param{dur};
264             }
265             }
266 19 100       30 if ( defined $open ) {
267 8         13 push @midi, [ 'note_off', $delay, $param{chan}, $open, 0 ];
268 8         17 $delay = 0;
269             }
270              
271             # trailing rests (e.g. in a [1000] pattern) create a delay that
272             # must be applied to the start of subsequent repeats of this
273             # measure (if there is an onset that makes this possible) and
274             # then must be passed on as leftovers for the next text_event
275             #
276             # NOTE this duplicates the MIDI events by default (unless embig)
277 19 100 100     54 if ( $delay and $onsets and $ttl > 1 ) {
      100        
278 3         10 push $events->@*, @midi;
279 3         5 $midi[0] = [ $midi[0]->@* ];
280 3         6 $midi[0][1] += $delay;
281 3 100       7 if ( $param{embig} ) {
282 1         4 _to_midi_bigly( $events, \@midi, $ttl );
283             } else {
284 2         6 push $events->@*, (@midi) x ( $ttl - 1 );
285             }
286             } else {
287 16 100       27 if ( $param{embig} ) {
288 2         5 _to_midi_bigly( $events, \@midi, $ttl );
289             } else {
290 14         42 push $events->@*, (@midi) x $ttl;
291             }
292             }
293              
294             # delay from trailing rests *or* a full measure of rest
295 19         41 $leftover = $delay;
296              
297             # remainder of full measures of rest, if any
298 19 100       40 $leftover += $bpat->@* * $param{dur} * ( $ttl - 1 ) unless $onsets;
299              
300 19         22 $maxm -= $ttl;
301 19 100       45 last if $maxm <= 0;
302             }
303              
304             # end of track event for sustain to have something to extend out to,
305             # and so that different trailing rests between different voices are
306             # less likely to exhibit ragged track ends. it also simplifies the
307             # handling of the last event in the stream, below
308 12         35 push $events->@*, [ 'text_event', $leftover, "v$id EOT\n" ];
309              
310             # and here the MIDI is modified if need be -- the above is already
311             # complicated, and it's (somewhat) easier to cut events out and
312             # fiddle with delays on the completed stream
313 12 100 100     53 if ( $param{sustain} or $param{notext} ) {
314 3         4 my $i = 0;
315 3         5 while ( $i < $events->$#* ) {
316 36 100 100     102 if ( $param{sustain} and $events->[$i][0] eq 'note_off' ) {
    100 100        
317             # extend delay on the note_off to the next note_on;
318             # there might be a text_event between
319 10         12 my $delay = 0;
320 10         12 my $j = $i + 1;
321 10         30 while (1) {
322 12 100 100     31 if ( $events->[$j][EVENT] eq 'text_event' and $events->[$j][DTIME] > 0 ) {
    100          
323 1         1 $delay += $events->[$j][DTIME];
324 1         2 $events->[$j][DTIME] = 0;
325             } elsif ( $events->[$j][EVENT] eq 'note_on' ) {
326 9 100       14 if ( $events->[$j][DTIME] > 0 ) {
327 4         6 $delay += $events->[$j][DTIME];
328 4         6 $events->[$j] = [ $events->[$j]->@* ];
329 4         5 $events->[$j][DTIME] = 0;
330             }
331 9         9 last;
332             }
333 3 100       4 last if ++$j > $events->$#*;
334             }
335 10         25 $events->[$i] = [ $events->[$i]->@* ];
336 10         12 $events->[$i][DTIME] += $delay;
337              
338             } elsif ( $param{notext} and $events->[$i][EVENT] eq 'text_event' ) {
339 2         2 my $delay = $events->[$i][DTIME];
340 2         5 splice $events->@*, $i, 1;
341 2         5 $events->[$i] = [ $events->[$i]->@* ];
342 2         2 $events->[$i][DTIME] += $delay;
343 2         4 next; # examine the new event at the current index
344             }
345 34         45 $i++;
346             }
347              
348             # assume the final event is the EOT text_event
349 3 100       7 pop $events->@* if $param{notext};
350             }
351              
352 12         56 return $track;
353             }
354              
355             sub _to_midi_bigly {
356 3     3   4 my ( $events, $midi, $ttl ) = @_;
357 3         5 for ( 1 .. $ttl ) {
358 6         8 for my $eref ( $midi->@* ) {
359 20         42 push $events->@*, [ $eref->@* ];
360             }
361             }
362             }
363              
364             sub to_string {
365 10     10 1 1567 my ( $self, %param ) = @_;
366              
367 10         23 my $replay = $self->replay;
368 10 100 100     355 croak "empty replay log"
      100        
369             unless defined $replay
370             and ref $replay eq 'ARRAY'
371             and $replay->@*;
372              
373 7   100     28 $param{divisor} //= 1;
374 7   100     22 $param{rs} //= "\n";
375 7   100     27 $param{sep} //= "\t";
376              
377 7         8 my $beat = 0;
378 7   100     19 my $id = $self->id // '';
379 7   100     20 my $maxm = $param{maxm} // ~0;
380 7         8 my $str = '';
381              
382 7         13 for my $ref ( $replay->@* ) {
383 12         18 my ( $bpat, $ttl ) = $ref->@*;
384 12         31 my $bstr = join( '', $bpat->@* ) =~ tr/10/x./r;
385 12 100       18 $ttl = $maxm if $ttl > $maxm;
386              
387             $str .=
388 12         50 join( $param{sep}, $beat / $param{divisor}, $id, $bstr, $ttl ) . $param{rs};
389              
390 12         14 $beat += $ttl * $bpat->@*;
391 12         28 $maxm -= $ttl;
392 12 100       26 last if $maxm <= 0;
393             }
394              
395 7         26 return $str;
396             }
397              
398             1;
399             __END__