File Coverage

blib/lib/Music/RhythmSet/Voice.pm
Criterion Covered Total %
statement 196 196 100.0
branch 84 84 100.0
condition 101 101 100.0
subroutine 15 15 100.0
pod 7 7 100.0
total 403 403 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.03';
8              
9 3     3   1313 use 5.24.0;
  3         11  
10 3     3   17 use warnings;
  3         6  
  3         101  
11 3     3   17 use Carp qw(confess croak);
  3         6  
  3         151  
12 3     3   1145 use MIDI;
  3         26128  
  3         87  
13 3     3   1209 use Moo;
  3         23658  
  3         18  
14 3     3   4376 use namespace::clean;
  3         23536  
  3         18  
15              
16 3     3   937 use constant { NOTE_ON => 1, NOTE_OFF => 0, EVENT => 0, DTIME => 1 };
  3         6  
  3         3074  
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 231 my ( $self, $args ) = @_;
29 46 100 100     272 if ( exists $args->{pattern} and exists $args->{ttl} ) {
30 9 100       319 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     402 and $args->{pattern}->@*;
      100        
35 4         35 push $self->replay->@*, [ $args->{pattern}, $args->{ttl} ];
36             }
37             }
38              
39             ########################################################################
40             #
41             # METHODS
42              
43             sub advance {
44 28     28 1 8443 my ( $self, $count, %param ) = @_;
45              
46 28         60 my $measure = $self->measure;
47              
48 28   100     105 for ( 1 .. $count // 1 ) {
49 36         73 my $ttl = $self->ttl - 1;
50              
51 36         59 $param{measure} = $measure++;
52 36         70 $param{pattern} = $self->pattern;
53              
54 36 100       84 if ( $ttl <= 0 ) {
55 22         43 my $next = $self->next;
56              
57 22 100 100     301 confess "no next callback"
58             unless defined $next and ref $next eq 'CODE';
59              
60 20         63 ( $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     490 and $param{pattern}->@*;
      100        
66 17 100       131 confess "invalid ttl" if $ttl < 1;
67              
68 16         35 $self->pattern( $param{pattern} );
69              
70 16         42 push $self->replay->@*, [ $param{pattern}, $ttl ];
71             }
72              
73 30         63 $self->ttl($ttl);
74             }
75              
76 22         44 $self->measure($measure);
77              
78 22         56 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 1929 my ( $self, %param ) = @_;
91              
92 10   100     59 $param{newid} //= $self->id;
93              
94             my $new = Music::RhythmSet::Voice->new(
95             id => $param{newid},
96 10         26 map { $_, scalar $self->$_ } qw(next measure ttl),
  30         301  
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         30 my $pat = $self->pattern;
105 10 100       29 if ( defined $pat ) {
106 6 100 100     47 die "invalid pattern" unless ref $pat eq 'ARRAY' and $pat->@*;
107 4         16 $new->pattern( [ $pat->@* ] );
108             }
109              
110 8         18 my $ref = $self->replay;
111 8 100       36 if ( defined $ref ) {
112 7 100       31 die "replay must be an array reference"
113             unless ref $ref eq 'ARRAY';
114 6 100       27 die "replay array must contain array references"
115             unless ref $ref->[0] eq 'ARRAY';
116 5         13 $new->replay( [ map { [ [ $_->[0]->@* ], $_->[1] ] } $ref->@* ] );
  13         68  
117             }
118              
119 6         27 return $new;
120             }
121              
122             sub from_string {
123 11     11 1 2439 my ( $self, $str, %param ) = @_;
124 11 100 100     315 croak "need a string" unless defined $str and length $str;
125              
126 9   100     35 $param{rs} //= "\n";
127 9 100       19 if ( $param{sep} ) {
128 2         30 $param{sep} = qr/\Q$param{sep}\E/;
129             } else {
130 7         28 $param{sep} = qr/\s+/;
131             }
132              
133 9         20 my $linenum = 1;
134 9         12 my @newplay;
135              
136 9         64 for my $line ( split /\Q$param{rs}/, $str ) {
137 22 100       81 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       301 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 3     3   1555 push @newplay, [ [ split //, $+{bstr} =~ tr/x./10/r ], $+{ttl} ];
  3         1093  
  3         4443  
  14         133  
151             } else {
152 4         345 croak "invalid record at line $linenum";
153             }
154 14         50 $linenum++;
155             }
156              
157 5         20 push $self->replay->@*, @newplay;
158              
159 5         64 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 1609 my ( $self, %param ) = @_;
166              
167 10         25 my $replay = $self->replay;
168 10 100 100     350 croak "empty replay log"
      100        
169             unless defined $replay
170             and ref $replay eq 'ARRAY'
171             and $replay->@*;
172              
173 7   100     31 $param{dur} //= '16';
174 7   100     26 $param{note} //= 'c';
175 7   100     27 $param{rest} //= 'r';
176              
177 7   100     24 my $id = $self->id // '';
178 7         12 my $ly = '';
179 7   100     17 my $maxm = $param{maxm} // ~0;
180              
181 7         19 for my $ref ( $replay->@* ) {
182 10         21 my ( $bpat, $ttl ) = $ref->@*;
183 10 100       22 $ttl = $maxm if $ttl > $maxm;
184              
185 10         48 $ly .= " % v$id " . join( '', $bpat->@* ) =~ tr/10/x./r . " $ttl\n";
186 10 100       39 if ( $param{time} ) {
187 2         6 $ly .= ' \time ' . $bpat->@* . '/' . $param{time} . "\n";
188             }
189 10         21 my $str = ' ';
190 10         27 for my $x ( $bpat->@* ) {
191 26 100       53 if ( $x == NOTE_ON ) {
192 16         31 $str .= ' ' . $param{note} . $param{dur};
193             } else {
194 10         25 $str .= ' ' . $param{rest} . $param{dur};
195             }
196             }
197 10         29 $ly .= join( "\n", ($str) x $ttl ) . "\n";
198              
199 10         15 $maxm -= $ttl;
200 10 100       34 last if $maxm <= 0;
201             }
202 7         46 return $ly;
203             }
204              
205             sub to_midi {
206 14     14 1 9344 my ( $self, %param ) = @_;
207              
208 14         41 my $replay = $self->replay;
209 14 100 100     331 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 11   100     53 $param{chan} //= 0;
216 11   100     50 $param{dur} //= 20;
217 11   100     38 $param{note} //= 60;
218 11   100     55 $param{tempo} //= 500_000;
219 11   100     83 $param{velo} //= 90; # "default value" per lilypond scm/midi.scm
220              
221 11         53 my $track = MIDI::Track->new;
222 11         292 my $events = $track->events_r;
223              
224 11         86 my $delay;
225 11   100     40 my $id = $self->id // '';
226 11         19 my $leftover = 0;
227 11   100     30 my $maxm = $param{maxm} // ~0;
228              
229 11 100       75 push $events->@*, [ 'track_name', 0, 'voice' . ( length $id ? " $id" : '' ) ];
230 11         27 push $events->@*, [ 'set_tempo', 0, $param{tempo} ];
231              
232 11         25 for my $ref ( $replay->@* ) {
233 16         47 my ( $bpat, $ttl ) = $ref->@*;
234 16 100       38 $ttl = $maxm if $ttl > $maxm;
235              
236 16         74 push $events->@*,
237             [ 'text_event', $leftover,
238             "v$id " . join( '', $bpat->@* ) =~ tr/10/x./r . " $ttl\n"
239             ];
240              
241 16         31 $delay = 0;
242 16         24 my ( $onsets, $open, @midi );
243              
244 16         31 for my $x ( $bpat->@* ) {
245 38 100       71 if ( $x == NOTE_ON ) {
246 20         28 $onsets++;
247 20 100       37 if ( defined $open ) {
248 8         15 push @midi, [ 'note_off', $delay, $param{chan}, $open, 0 ];
249 8         13 $delay = 0;
250             }
251 20         44 push @midi, [ 'note_on', $delay, map { $param{$_} } qw(chan note velo) ];
  60         125  
252 20         30 $delay = $param{dur};
253 20         37 $open = $param{note};
254             } else {
255 18 100       40 if ( defined $open ) {
256 6         18 push @midi, [ 'note_off', $delay, $param{chan}, $open, 0 ];
257 6         11 $delay = 0;
258 6         10 undef $open;
259             }
260 18         32 $delay += $param{dur};
261             }
262             }
263 16 100       34 if ( defined $open ) {
264 6         15 push @midi, [ 'note_off', $delay, $param{chan}, $open, 0 ];
265 6         10 $delay = 0;
266             }
267              
268             # trailing rests (e.g. in a [1000] pattern) create a delay that
269             # must be applied to the start of subsequent repeats of this
270             # measure (if there is an onset that makes this possible) and
271             # then must be passed on as leftovers for the next text_event
272 16 100 100     66 if ( $delay and $onsets and $ttl > 1 ) {
      100        
273 2         6 push $events->@*, @midi;
274 2         5 $midi[0] = [ $midi[0]->@* ];
275 2         3 $midi[0][1] += $delay;
276 2         7 push $events->@*, (@midi) x ( $ttl - 1 );
277             } else {
278 14         48 push $events->@*, (@midi) x $ttl;
279             }
280              
281             # delay from trailing rests *or* a full measure of rest
282 16         23 $leftover = $delay;
283              
284             # remainder of full measures of rest, if any
285 16 100       31 $leftover += $bpat->@* * $param{dur} * ( $ttl - 1 ) unless $onsets;
286              
287 16         25 $maxm -= $ttl;
288 16 100       42 last if $maxm <= 0;
289             }
290              
291             # end of track event for sustain to have something to extend out to,
292             # and so that different trailing rests between different voices are
293             # less likely to exhibit ragged track ends. it also simplifies the
294             # handling of the last event in the stream, below
295 11         34 push $events->@*, [ 'text_event', $leftover, "v$id EOT\n" ];
296              
297             # and here the MIDI is modified if need be -- the above is already
298             # complicated, and it's (somewhat) easier to cut events out and
299             # fiddle with delays on the completed stream
300 11 100 100     50 if ( $param{sustain} or $param{notext} ) {
301 3         5 my $i = 0;
302 3         10 while ( $i < $events->$#* ) {
303 35 100 100     132 if ( $param{sustain} and $events->[$i][0] eq 'note_off' ) {
    100 100        
304             # extend delay on the note_off to the next note_on;
305             # there might be a text_event between
306 10         15 my $delay = 0;
307 10         13 my $j = $i + 1;
308 10         13 while (1) {
309 12 100 100     53 if ( $events->[$j][EVENT] eq 'text_event' and $events->[$j][DTIME] > 0 ) {
    100          
310 1         2 $delay += $events->[$j][DTIME];
311 1         3 $events->[$j][DTIME] = 0;
312             } elsif ( $events->[$j][EVENT] eq 'note_on' ) {
313 9 100       18 if ( $events->[$j][DTIME] > 0 ) {
314 4         6 $delay += $events->[$j][DTIME];
315 4         17 $events->[$j] = [ $events->[$j]->@* ];
316 4         7 $events->[$j][DTIME] = 0;
317             }
318 9         13 last;
319             }
320 3 100       7 last if ++$j > $events->$#*;
321             }
322 10         27 $events->[$i] = [ $events->[$i]->@* ];
323 10         19 $events->[$i][DTIME] += $delay;
324              
325             } elsif ( $param{notext} and $events->[$i][EVENT] eq 'text_event' ) {
326 2         4 my $delay = $events->[$i][DTIME];
327 2         6 splice $events->@*, $i, 1;
328 2         7 $events->[$i] = [ $events->[$i]->@* ];
329 2         4 $events->[$i][DTIME] += $delay;
330 2         6 next; # examine the new event at the current index
331             }
332 33         64 $i++;
333             }
334              
335             # assume the final event is the EOT text_event
336 3 100       7 pop $events->@* if $param{notext};
337             }
338              
339 11         68 return $track;
340             }
341              
342             sub to_string {
343 10     10 1 2040 my ( $self, %param ) = @_;
344              
345 10         31 my $replay = $self->replay;
346 10 100 100     339 croak "empty replay log"
      100        
347             unless defined $replay
348             and ref $replay eq 'ARRAY'
349             and $replay->@*;
350              
351 7   100     34 $param{divisor} //= 1;
352 7   100     24 $param{rs} //= "\n";
353 7   100     29 $param{sep} //= "\t";
354              
355 7         10 my $beat = 0;
356 7   100     23 my $id = $self->id // '';
357 7   100     38 my $maxm = $param{maxm} // ~0;
358 7         13 my $str = '';
359              
360 7         26 for my $ref ( $replay->@* ) {
361 12         27 my ( $bpat, $ttl ) = $ref->@*;
362 12         37 my $bstr = join( '', $bpat->@* ) =~ tr/10/x./r;
363 12 100       32 $ttl = $maxm if $ttl > $maxm;
364              
365             $str .=
366 12         63 join( $param{sep}, $beat / $param{divisor}, $id, $bstr, $ttl ) . $param{rs};
367              
368 12         24 $beat += $ttl * $bpat->@*;
369 12         16 $maxm -= $ttl;
370 12 100       32 last if $maxm <= 0;
371             }
372              
373 7         29 return $str;
374             }
375              
376             1;
377             __END__