File Coverage

blib/lib/Music/RhythmSet.pm
Criterion Covered Total %
statement 124 124 100.0
branch 32 32 100.0
condition 35 35 100.0
subroutine 18 18 100.0
pod 10 10 100.0
total 219 219 100.0


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # sets of rhythms, comprised of one or more voices (or tracks) and
4             # various utility functions
5              
6             package Music::RhythmSet;
7             our $VERSION = '0.05';
8              
9 2     2   231625 use 5.24.0;
  2         12  
10 2     2   11 use warnings;
  2         3  
  2         58  
11 2     2   9 use Carp qw(croak);
  2         382  
  2         99  
12 2     2   1015 use List::GroupingPriorityQueue qw(grpriq_add);
  2         2587  
  2         121  
13 2     2   505 use MIDI;
  2         13398  
  2         49  
14 2     2   540 use Moo;
  2         11744  
  2         10  
15 2     2   2400 use namespace::clean;
  2         11743  
  2         16  
16              
17 2     2   1140 use Music::RhythmSet::Voice;
  2         12  
  2         3604  
18              
19             has stash => ( is => 'rw' );
20             has voices => ( is => 'rw', default => sub { [] } );
21              
22             # perldoc Moo
23             sub BUILD {
24 11     11 1 50 my ( $self, $args ) = @_;
25             # so ->new->add(...) can instead be written ->new(voicel => [...])
26 11 100       79 if ( exists $args->{voicel} ) {
27             croak "invalid voicel"
28             unless defined $args->{voicel}
29 3 100 100     231 and ref $args->{voicel} eq 'ARRAY';
30 1         5 $self->add( $args->{voicel}->@* );
31 1         9 delete $args->{voicel};
32             }
33             }
34              
35             ########################################################################
36             #
37             # METHODS
38              
39             sub add {
40 15     15 1 4526 my ( $self, @rest ) = @_;
41 15 100       125 croak "nothing to add" unless @rest;
42              
43 14         39 my $maxid = $self->voices->$#*;
44              
45 14         28 for my $ref (@rest) {
46 16 100 100     231 croak "invalid voice parameters"
47             unless defined $ref and ref $ref eq 'HASH';
48 14         31 $ref->{id} = ++$maxid;
49 14         277 push $self->voices->@*, Music::RhythmSet::Voice->new( $ref->%* );
50             }
51              
52 12         29 return $self;
53             }
54              
55             sub advance {
56 4     4 1 2760 my ( $self, $count, %param ) = @_;
57             # this is done stepwise for each voice so that TTL expirations and
58             # thus potential new patterns are more likely to be visible to other
59             # voices. voices that depend on other voices should therefore be
60             # added after those other voices (or there could be a two- or N-pass
61             # system to resolve any inter-voice pattern generation difficulties,
62             # but that's not supported here)
63 4   100     19 for ( 1 .. $count // 1 ) {
64 11         24 for my $voice ( $self->voices->@* ) {
65 19         33 $param{set} = $self;
66 19         46 $voice->advance( 1, %param );
67             }
68             }
69 4         46 return $self;
70             }
71              
72             sub changes {
73 6     6 1 6123 my ( $self, %param ) = @_;
74              
75 6         13 for my $cb (qw{header voice}) {
76             croak "need $cb callback"
77             unless defined $param{$cb}
78 10 100 100     372 and ref $param{$cb} eq 'CODE';
79             }
80              
81             # patterns can be of different lengths between voices (or can vary
82             # over time inside a voice), though may be the same in which case
83             # the caller can divide the beat count by however many beats there
84             # are in a measure to obtain the measure number. otherwise, the
85             # "measure" is the number of beats since the start of the replay log
86 2   100     28 $param{divisor} //= 1;
87 2   100     19 $param{max} //= ~0;
88              
89 2         6 my $queue = [];
90              
91 2         8 for my $voice ( $self->voices->@* ) {
92 3         5 my $beat = 0;
93 3         12 for my $ref ( $voice->replay->@* ) {
94 8         12 my ( $bpat, $ttl ) = $ref->@*;
95             # build a priority queue of when voices change their pattern
96 8         30 grpriq_add( $queue, $beat, [ $voice->id, $bpat ] );
97 8         129 $beat += $ttl * $bpat->@*;
98             }
99             }
100              
101 2         5 my ( @curpat, @curpat_str );
102              
103             # parse the queue for pattern changes and let the caller decide how
104             # to act on the results (see eg/beatinator for one way)
105 2         4 for my $entry ( $queue->@* ) { # [[id,[bp]],...],beats
106 7         43 my $measure = $entry->[1] / $param{divisor};
107 7 100       17 last if $measure >= $param{max};
108              
109 6         10 my ( @changed, @repeat );
110              
111 6         23 for my $ref ( $entry->[0]->@* ) {
112 7         13 my ( $id, $bpat ) = $ref->@*;
113 7         11 $changed[$id] = 1;
114 7         13 $curpat[$id] = $bpat;
115 7         19 my $bstr = join( '', $bpat->@* ) =~ tr/10/x./r;
116 7 100 100     35 if ( $bstr eq ( $curpat_str[$id] // '' ) ) {
117 1         3 $repeat[$id] = 1;
118             }
119 7         16 $curpat_str[$id] = $bstr;
120             }
121              
122 6         17 $param{header}->($measure);
123              
124 6         29 for my $id ( 0 .. $#curpat ) {
125 9         59 $param{voice}->(
126             $measure, $id, $curpat[$id], $curpat_str[$id], $changed[$id], $repeat[$id]
127             );
128             }
129             }
130              
131 2         22 return $self;
132             }
133              
134             sub clone {
135 1     1 1 495 my ($self) = @_;
136              
137 1         26 my $new = Music::RhythmSet->new;
138 1         2 my @voices;
139              
140 1         5 for my $voice ( $self->voices->@* ) {
141 2         7 push @voices, $voice->clone;
142             }
143              
144 1         5 $new->voices( \@voices );
145              
146 1         3 return $new;
147             }
148              
149             sub from_string {
150 14     14 1 4421 my ( $self, $str, %param ) = @_;
151 14 100 100     317 croak "need a string" unless defined $str and length $str;
152              
153 12   100     47 $param{rs} //= "\n";
154 12 100       34 if ( $param{sep} ) {
155 2         15 $param{sep} = qr/\Q$param{sep}\E/;
156             } else {
157 10         37 $param{sep} = qr/\s+/;
158             }
159              
160 12         21 my $linenum = 1;
161 12         16 my @newplay;
162 12         58 my $voices = $self->voices;
163              
164 12         68 for my $line ( split /\Q$param{rs}/, $str ) {
165 21 100       83 next if $line =~ m/^\s*(?:#|$)/;
166             # the limits are to prevent overly long strings from being
167             # parsed; if this is a problem write a modified from_string that
168             # does allow such inputs, or modify the unused count
169 16 100       297 if ($line =~ m/^
170             (?\d{1,10}) $param{sep}
171             (?\d{1,3}) $param{sep}
172             (?[x.]{1,256}) $param{sep}
173             (?\d{1,5}) \s*(?:[#].*)?
174             $/ax
175             ) {
176             # only +1 ID over max is allowed to avoid creating a sparse
177             # voices list; this means that input that starts with voice
178             # 1 (or higher) will be rejected, or if voice 4 is seen
179             # before the first entry for voice 3 that too will be
180             # rejected. this might happen if a sort reordered the events
181             # and there was not a sub-sort to keep the voice IDs in
182             # ascending order
183 10 100 100     93 if ( $voices->$#* == 0 or $+{id} == $voices->$#* + 1 ) {
    100          
184 8         27 $self->add( {} );
185             } elsif ( $+{id} > $voices->$#* ) {
186 1         87 croak "ID out of range '$+{id}' at line $linenum";
187             }
188 9         99 push $newplay[ $+{id} ]->@*, [ [ split //, $+{bstr} =~ tr/x./10/r ], $+{ttl} ];
189             } else {
190 6         495 croak "invalid record at line $linenum";
191             }
192 9         41 $linenum++;
193             }
194              
195             # this complication is to make changes to the replay log more atomic
196             # given that the above can die mid-parse. the newplay array can be
197             # sparse e.g. if four voices already exist and the input only has
198             # records for voices 0 and 2
199 5         18 for my $id ( 0 .. $#newplay ) {
200 10 100       35 push $voices->[$id]->replay->@*, $newplay[$id]->@* if defined $newplay[$id];
201             }
202              
203 5         40 return $self;
204             }
205              
206             sub measure {
207 1     1 1 1392 my ( $self, $num ) = @_;
208 1         6 for my $voice ( $self->voices->@* ) {
209 2         6 $voice->measure($num);
210             }
211 1         4 return $self;
212             }
213              
214             sub to_ly {
215 2     2 1 753 my ( $self, %param ) = @_;
216              
217 2         14 for my $id ( 0 .. $self->voices->$#* ) {
218 4         10 for my $pram (qw/dur maxm note rest time/) {
219             $param{voice}[$id]{$pram} = $param{$pram}
220 20 100 100     62 if exists $param{$pram} and not exists $param{voice}[$id]{$pram};
221             }
222             }
223              
224 2         4 my $id = 0;
225 2         5 return [ map { $_->to_ly( $param{voice}->[ $id++ ]->%* ) } $self->voices->@* ];
  4         21  
226             }
227              
228             sub to_midi {
229 2     2 1 618 my ( $self, %param ) = @_;
230              
231 2   100     10 $param{format} //= 1;
232 2   100     10 $param{ticks} //= 96;
233              
234 2         9 for my $id ( 0 .. $self->voices->$#* ) {
235 4         10 for
236             my $pram (qw/chan dur embig maxm note notext tempo sustain velo patch_change/)
237             {
238             $param{track}[$id]{$pram} = $param{$pram}
239 40 100 100     91 if exists $param{$pram} and not exists $param{track}[$id]{$pram};
240             }
241             }
242              
243 2         4 my $id = 0;
244             return MIDI::Opus->new(
245             { format => $param{format},
246             ticks => $param{ticks},
247             tracks =>
248 2         8 [ map { $_->to_midi( $param{track}->[ $id++ ]->%* ) } $self->voices->@* ]
  4         22  
249             }
250             );
251             }
252              
253             sub to_string {
254 2     2 1 2432 my ( $self, @rest ) = @_;
255              
256 2         6 my $str = '';
257              
258 2         8 for my $voice ( $self->voices->@* ) {
259 4         14 $str .= $voice->to_string(@rest);
260             }
261              
262 2         8 return $str;
263             }
264              
265             1;
266             __END__