File Coverage

blib/lib/Music/RhythmSet/Voice.pm
Criterion Covered Total %
statement 206 206 100.0
branch 90 90 100.0
condition 101 101 100.0
subroutine 16 16 100.0
pod 7 7 100.0
total 420 420 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.05';
8              
9 3     3   1332 use 5.24.0;
  3         13  
10 3     3   22 use warnings;
  3         14  
  3         137  
11 3     3   21 use Carp qw(confess croak);
  3         6  
  3         165  
12 3     3   1013 use MIDI;
  3         26447  
  3         127  
13 3     3   1183 use Moo;
  3         23534  
  3         17  
14 3     3   4364 use namespace::clean;
  3         23667  
  3         23  
15              
16 3     3   950 use constant { NOTE_ON => 1, NOTE_OFF => 0, EVENT => 0, DTIME => 1 };
  3         7  
  3         3029  
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 246 my ( $self, $args ) = @_;
29 46 100 100     272 if ( exists $args->{pattern} and exists $args->{ttl} ) {
30 9 100       334 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     388 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 8492 my ( $self, $count, %param ) = @_;
45              
46 28         57 my $measure = $self->measure;
47              
48 28   100     84 for ( 1 .. $count // 1 ) {
49 36         62 my $ttl = $self->ttl - 1;
50              
51 36         61 $param{measure} = $measure++;
52 36         59 $param{pattern} = $self->pattern;
53              
54 36 100       71 if ( $ttl <= 0 ) {
55 22         47 my $next = $self->next;
56              
57 22 100 100     305 confess "no next callback"
58             unless defined $next and ref $next eq 'CODE';
59              
60 20         64 ( $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     528 and $param{pattern}->@*;
      100        
66 17 100       125 confess "invalid ttl" if $ttl < 1;
67              
68 16         32 $self->pattern( $param{pattern} );
69              
70 16         55 push $self->replay->@*, [ $param{pattern}, $ttl ];
71             }
72              
73 30         65 $self->ttl($ttl);
74             }
75              
76 22         39 $self->measure($measure);
77              
78 22         58 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 1924 my ( $self, %param ) = @_;
91              
92 10   100     54 $param{newid} //= $self->id;
93              
94             my $new = Music::RhythmSet::Voice->new(
95             id => $param{newid},
96 10         25 map { $_, scalar $self->$_ } qw(next measure ttl),
  30         335  
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         34 my $pat = $self->pattern;
105 10 100       27 if ( defined $pat ) {
106 6 100 100     46 die "invalid pattern" unless ref $pat eq 'ARRAY' and $pat->@*;
107 4         16 $new->pattern( [ $pat->@* ] );
108             }
109              
110 8         31 my $ref = $self->replay;
111 8 100       22 if ( defined $ref ) {
112 7 100       37 die "replay must be an array reference"
113             unless ref $ref eq 'ARRAY';
114 6 100       28 die "replay array must contain array references"
115             unless ref $ref->[0] eq 'ARRAY';
116 5         10 $new->replay( [ map { [ [ $_->[0]->@* ], $_->[1] ] } $ref->@* ] );
  13         60  
117             }
118              
119 6         29 return $new;
120             }
121              
122             sub from_string {
123 11     11 1 2432 my ( $self, $str, %param ) = @_;
124 11 100 100     323 croak "need a string" unless defined $str and length $str;
125              
126 9   100     35 $param{rs} //= "\n";
127 9 100       22 if ( $param{sep} ) {
128 2         19 $param{sep} = qr/\Q$param{sep}\E/;
129             } else {
130 7         37 $param{sep} = qr/\s+/;
131             }
132              
133 9         19 my $linenum = 1;
134 9         13 my @newplay;
135              
136 9         77 for my $line ( split /\Q$param{rs}/, $str ) {
137 22 100       78 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       326 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   1957 push @newplay, [ [ split //, $+{bstr} =~ tr/x./10/r ], $+{ttl} ];
  3         1208  
  3         4988  
  14         135  
151             } else {
152 4         337 croak "invalid record at line $linenum";
153             }
154 14         90 $linenum++;
155             }
156              
157 5         24 push $self->replay->@*, @newplay;
158              
159 5         34 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 1550 my ( $self, %param ) = @_;
166              
167 10         27 my $replay = $self->replay;
168 10 100 100     341 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     24 $param{note} //= 'c';
175 7   100     29 $param{rest} //= 'r';
176              
177 7   100     35 my $id = $self->id // '';
178 7         12 my $ly = '';
179 7   100     21 my $maxm = $param{maxm} // ~0;
180              
181 7         17 for my $ref ( $replay->@* ) {
182 10         21 my ( $bpat, $ttl ) = $ref->@*;
183 10 100       24 $ttl = $maxm if $ttl > $maxm;
184              
185 10         50 $ly .= " % v$id " . join( '', $bpat->@* ) =~ tr/10/x./r . " $ttl\n";
186 10 100       29 if ( $param{time} ) {
187 2         6 $ly .= ' \time ' . $bpat->@* . '/' . $param{time} . "\n";
188             }
189 10         17 my $str = ' ';
190 10         18 for my $x ( $bpat->@* ) {
191 26 100       60 if ( $x == NOTE_ON ) {
192 16         31 $str .= ' ' . $param{note} . $param{dur};
193             } else {
194 10         22 $str .= ' ' . $param{rest} . $param{dur};
195             }
196             }
197 10         44 $ly .= join( "\n", ($str) x $ttl ) . "\n";
198              
199 10         16 $maxm -= $ttl;
200 10 100       27 last if $maxm <= 0;
201             }
202 7         43 return $ly;
203             }
204              
205             sub to_midi {
206 15     15 1 11126 my ( $self, %param ) = @_;
207              
208 15         56 my $replay = $self->replay;
209 15 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 12   100     58 $param{chan} //= 0;
216 12   100     41 $param{dur} //= 20;
217 12   100     45 $param{note} //= 60;
218 12   100     79 $param{tempo} //= 500_000;
219 12   100     80 $param{velo} //= 90; # "default value" per lilypond scm/midi.scm
220              
221 12         57 my $track = MIDI::Track->new;
222 12         320 my $events = $track->events_r;
223              
224 12         87 my $delay;
225 12   100     47 my $id = $self->id // '';
226 12         22 my $leftover = 0;
227 12   100     38 my $maxm = $param{maxm} // ~0;
228              
229 12 100       89 push $events->@*, [ 'track_name', 0, 'voice' . ( length $id ? " $id" : '' ) ];
230 12         33 push $events->@*, [ 'set_tempo', 0, $param{tempo} ];
231 12 100       30 if ( $param{patch_change} ) {
232 1         5 push $events->@*, [ 'patch_change', 0, $param{chan}, $param{patch_change} ];
233             }
234              
235 12         51 for my $ref ( $replay->@* ) {
236 19         43 my ( $bpat, $ttl ) = $ref->@*;
237 19 100       44 $ttl = $maxm if $ttl > $maxm;
238              
239 19         87 push $events->@*,
240             [ 'text_event', $leftover,
241             "v$id " . join( '', $bpat->@* ) =~ tr/10/x./r . " $ttl\n"
242             ];
243              
244 19         37 $delay = 0;
245 19         33 my ( $onsets, $open, @midi );
246              
247 19         31 for my $x ( $bpat->@* ) {
248 46 100       89 if ( $x == NOTE_ON ) {
249 26         35 $onsets++;
250 26 100       56 if ( defined $open ) {
251 11         24 push @midi, [ 'note_off', $delay, $param{chan}, $open, 0 ];
252 11         15 $delay = 0;
253             }
254 26         54 push @midi, [ 'note_on', $delay, map { $param{$_} } qw(chan note velo) ];
  78         151  
255 26         43 $delay = $param{dur};
256 26         41 $open = $param{note};
257             } else {
258 20 100       43 if ( defined $open ) {
259 7         20 push @midi, [ 'note_off', $delay, $param{chan}, $open, 0 ];
260 7         15 $delay = 0;
261 7         11 undef $open;
262             }
263 20         33 $delay += $param{dur};
264             }
265             }
266 19 100       39 if ( defined $open ) {
267 8         19 push @midi, [ 'note_off', $delay, $param{chan}, $open, 0 ];
268 8         15 $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     107 if ( $delay and $onsets and $ttl > 1 ) {
      100        
278 3         9 push $events->@*, @midi;
279 3         7 $midi[0] = [ $midi[0]->@* ];
280 3         7 $midi[0][1] += $delay;
281 3 100       8 if ( $param{embig} ) {
282 1         4 _to_midi_bigly( $events, \@midi, $ttl );
283             } else {
284 2         8 push $events->@*, (@midi) x ( $ttl - 1 );
285             }
286             } else {
287 16 100       35 if ( $param{embig} ) {
288 2         9 _to_midi_bigly( $events, \@midi, $ttl );
289             } else {
290 14         34 push $events->@*, (@midi) x $ttl;
291             }
292             }
293              
294             # delay from trailing rests *or* a full measure of rest
295 19         25 $leftover = $delay;
296              
297             # remainder of full measures of rest, if any
298 19 100       54 $leftover += $bpat->@* * $param{dur} * ( $ttl - 1 ) unless $onsets;
299              
300 19         29 $maxm -= $ttl;
301 19 100       87 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         57 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     64 if ( $param{sustain} or $param{notext} ) {
314 3         6 my $i = 0;
315 3         10 while ( $i < $events->$#* ) {
316 36 100 100     147 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         14 my $delay = 0;
320 10         13 my $j = $i + 1;
321 10         12 while (1) {
322 12 100 100     41 if ( $events->[$j][EVENT] eq 'text_event' and $events->[$j][DTIME] > 0 ) {
    100          
323 1         8 $delay += $events->[$j][DTIME];
324 1         3 $events->[$j][DTIME] = 0;
325             } elsif ( $events->[$j][EVENT] eq 'note_on' ) {
326 9 100       19 if ( $events->[$j][DTIME] > 0 ) {
327 4         11 $delay += $events->[$j][DTIME];
328 4         9 $events->[$j] = [ $events->[$j]->@* ];
329 4         7 $events->[$j][DTIME] = 0;
330             }
331 9         10 last;
332             }
333 3 100       11 last if ++$j > $events->$#*;
334             }
335 10         21 $events->[$i] = [ $events->[$i]->@* ];
336 10         18 $events->[$i][DTIME] += $delay;
337              
338             } elsif ( $param{notext} and $events->[$i][EVENT] eq 'text_event' ) {
339 2         6 my $delay = $events->[$i][DTIME];
340 2         7 splice $events->@*, $i, 1;
341 2         7 $events->[$i] = [ $events->[$i]->@* ];
342 2         4 $events->[$i][DTIME] += $delay;
343 2         7 next; # examine the new event at the current index
344             }
345 34         57 $i++;
346             }
347              
348             # assume the final event is the EOT text_event
349 3 100       9 pop $events->@* if $param{notext};
350             }
351              
352 12         96 return $track;
353             }
354              
355             sub _to_midi_bigly {
356 3     3   8 my ( $events, $midi, $ttl ) = @_;
357 3         9 for ( 1 .. $ttl ) {
358 6         7 for my $eref ( $midi->@* ) {
359 20         43 push $events->@*, [ $eref->@* ];
360             }
361             }
362             }
363              
364             sub to_string {
365 10     10 1 2012 my ( $self, %param ) = @_;
366              
367 10         28 my $replay = $self->replay;
368 10 100 100     297 croak "empty replay log"
      100        
369             unless defined $replay
370             and ref $replay eq 'ARRAY'
371             and $replay->@*;
372              
373 7   100     44 $param{divisor} //= 1;
374 7   100     29 $param{rs} //= "\n";
375 7   100     39 $param{sep} //= "\t";
376              
377 7         14 my $beat = 0;
378 7   100     21 my $id = $self->id // '';
379 7   100     22 my $maxm = $param{maxm} // ~0;
380 7         14 my $str = '';
381              
382 7         16 for my $ref ( $replay->@* ) {
383 12         26 my ( $bpat, $ttl ) = $ref->@*;
384 12         36 my $bstr = join( '', $bpat->@* ) =~ tr/10/x./r;
385 12 100       30 $ttl = $maxm if $ttl > $maxm;
386              
387             $str .=
388 12         62 join( $param{sep}, $beat / $param{divisor}, $id, $bstr, $ttl ) . $param{rs};
389              
390 12         25 $beat += $ttl * $bpat->@*;
391 12         18 $maxm -= $ttl;
392 12 100       30 last if $maxm <= 0;
393             }
394              
395 7         31 return $str;
396             }
397              
398             1;
399             __END__