File Coverage

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