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.06';
8              
9 2     2   164524 use 5.24.0;
  2         8  
10 2     2   10 use warnings;
  2         7  
  2         97  
11 2     2   12 use Carp qw(croak);
  2         2  
  2         101  
12 2     2   1331 use List::GroupingPriorityQueue qw(grpriq_add);
  2         2890  
  2         159  
13 2     2   374 use MIDI;
  2         10642  
  2         79  
14 2     2   442 use Moo;
  2         6256  
  2         13  
15 2     2   2202 use namespace::clean;
  2         13413  
  2         17  
16              
17 2     2   1171 use Music::RhythmSet::Voice;
  2         8  
  2         5869  
18              
19             has stash => ( is => 'rw' );
20             has voices => ( is => 'rw', default => sub { [] } );
21              
22             # perldoc Moo
23             sub BUILD {
24 11     11 1 71 my ( $self, $args ) = @_;
25             # so ->new->add(...) can instead be written ->new(voicel => [...])
26 11 100       98 if ( exists $args->{voicel} ) {
27             croak "invalid voicel"
28             unless defined $args->{voicel}
29 3 100 100     321 and ref $args->{voicel} eq 'ARRAY';
30 1         7 $self->add( $args->{voicel}->@* );
31 1         17 delete $args->{voicel};
32             }
33             }
34              
35             ########################################################################
36             #
37             # METHODS
38              
39             sub add {
40 15     15 1 4098 my ( $self, @rest ) = @_;
41 15 100       168 croak "nothing to add" unless @rest;
42              
43 14         43 my $maxid = $self->voices->$#*;
44              
45 14         29 for my $ref (@rest) {
46 16 100 100     262 croak "invalid voice parameters"
47             unless defined $ref and ref $ref eq 'HASH';
48 14         32 $ref->{id} = ++$maxid;
49 14         304 push $self->voices->@*, Music::RhythmSet::Voice->new( $ref->%* );
50             }
51              
52 12         31 return $self;
53             }
54              
55             sub advance {
56 4     4 1 2767 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     23 for ( 1 .. $count // 1 ) {
64 11         24 for my $voice ( $self->voices->@* ) {
65 19         29 $param{set} = $self;
66 19         47 $voice->advance( 1, %param );
67             }
68             }
69 4         27 return $self;
70             }
71              
72             sub changes {
73 6     6 1 4660 my ( $self, %param ) = @_;
74              
75 6         14 for my $cb (qw{header voice}) {
76             croak "need $cb callback"
77             unless defined $param{$cb}
78 10 100 100     687 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     9 $param{divisor} //= 1;
87 2   100     8 $param{max} //= ~0;
88              
89 2         5 my $queue = [];
90              
91 2         5 for my $voice ( $self->voices->@* ) {
92 3         5 my $beat = 0;
93 3         29 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         34 grpriq_add( $queue, $beat, [ $voice->id, $bpat ] );
97 8         137 $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         6 for my $entry ( $queue->@* ) { # [[id,[bp]],...],beats
106 7         38 my $measure = $entry->[1] / $param{divisor};
107 7 100       13 last if $measure >= $param{max};
108              
109 6         9 my ( @changed, @repeat );
110              
111 6         11 for my $ref ( $entry->[0]->@* ) {
112 7         12 my ( $id, $bpat ) = $ref->@*;
113 7         10 $changed[$id] = 1;
114 7         9 $curpat[$id] = $bpat;
115 7         36 my $bstr = join( '', $bpat->@* ) =~ tr/10/x./r;
116 7 100 100     38 if ( $bstr eq ( $curpat_str[$id] // '' ) ) {
117 1         2 $repeat[$id] = 1;
118             }
119 7         12 $curpat_str[$id] = $bstr;
120             }
121              
122 6         14 $param{header}->($measure);
123              
124 6         15 for my $id ( 0 .. $#curpat ) {
125 9         38 $param{voice}->(
126             $measure, $id, $curpat[$id], $curpat_str[$id], $changed[$id], $repeat[$id]
127             );
128             }
129             }
130              
131 2         28 return $self;
132             }
133              
134             sub clone {
135 1     1 1 314 my ($self) = @_;
136              
137 1         59 my $new = Music::RhythmSet->new;
138 1         2 my @voices;
139              
140 1         4 for my $voice ( $self->voices->@* ) {
141 2         9 push @voices, $voice->clone;
142             }
143              
144 1         4 $new->voices( \@voices );
145              
146 1         3 return $new;
147             }
148              
149             sub from_string {
150 14     14 1 3396 my ( $self, $str, %param ) = @_;
151 14 100 100     430 croak "need a string" unless defined $str and length $str;
152              
153 12   100     52 $param{rs} //= "\n";
154 12 100       25 if ( $param{sep} ) {
155 2         39 $param{sep} = qr/\Q$param{sep}\E/;
156             } else {
157 10         64 $param{sep} = qr/\s+/;
158             }
159              
160 12         19 my $linenum = 1;
161 12         18 my @newplay;
162 12         27 my $voices = $self->voices;
163              
164 12         126 for my $line ( split /\Q$param{rs}/, $str ) {
165 21 100       78 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       357 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     86 if ( $voices->$#* == 0 or $+{id} == $voices->$#* + 1 ) {
    100          
184 8         23 $self->add( {} );
185             } elsif ( $+{id} > $voices->$#* ) {
186 1         135 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         696 croak "invalid record at line $linenum";
191             }
192 9         49 $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         16 for my $id ( 0 .. $#newplay ) {
200 10 100       42 push $voices->[$id]->replay->@*, $newplay[$id]->@* if defined $newplay[$id];
201             }
202              
203 5         59 return $self;
204             }
205              
206             sub measure {
207 1     1 1 1205 my ( $self, $num ) = @_;
208 1         4 for my $voice ( $self->voices->@* ) {
209 2         6 $voice->measure($num);
210             }
211 1         3 return $self;
212             }
213              
214             sub to_ly {
215 2     2 1 615 my ( $self, %param ) = @_;
216              
217 2         8 for my $id ( 0 .. $self->voices->$#* ) {
218 4         7 for my $pram (qw/dur maxm note rest time/) {
219             $param{voice}[$id]{$pram} = $param{$pram}
220 20 100 100     67 if exists $param{$pram} and not exists $param{voice}[$id]{$pram};
221             }
222             }
223              
224 2         5 my $id = 0;
225 2         8 return [ map { $_->to_ly( $param{voice}->[ $id++ ]->%* ) } $self->voices->@* ];
  4         44  
226             }
227              
228             sub to_midi {
229 2     2 1 530 my ( $self, %param ) = @_;
230              
231 2   100     10 $param{format} //= 1;
232 2   100     6 $param{ticks} //= 96;
233              
234 2         9 for my $id ( 0 .. $self->voices->$#* ) {
235 4         6 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     69 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         7 [ map { $_->to_midi( $param{track}->[ $id++ ]->%* ) } $self->voices->@* ]
  4         16  
249             }
250             );
251             }
252              
253             sub to_string {
254 2     2 1 2205 my ( $self, @rest ) = @_;
255              
256 2         4 my $str = '';
257              
258 2         8 for my $voice ( $self->voices->@* ) {
259 4         49 $str .= $voice->to_string(@rest);
260             }
261              
262 2         7 return $str;
263             }
264              
265             1;
266             __END__