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.03';
8              
9 2     2   229312 use 5.24.0;
  2         13  
10 2     2   10 use warnings;
  2         4  
  2         59  
11 2     2   11 use Carp qw(croak);
  2         2  
  2         123  
12 2     2   1032 use List::GroupingPriorityQueue qw(grpriq_add);
  2         2547  
  2         120  
13 2     2   507 use MIDI;
  2         13332  
  2         57  
14 2     2   574 use Moo;
  2         11635  
  2         11  
15 2     2   2379 use namespace::clean;
  2         11634  
  2         13  
16              
17 2     2   1085 use Music::RhythmSet::Voice;
  2         5  
  2         3633  
18              
19             has stash => ( is => 'rw' );
20             has voices => ( is => 'rw', default => sub { [] } );
21              
22             # perldoc Moo
23             sub BUILD {
24 11     11 1 57 my ( $self, $args ) = @_;
25             # so ->new->add(...) can instead be written ->new(voicel => [...])
26 11 100       73 if ( exists $args->{voicel} ) {
27             croak "invalid voicel"
28             unless defined $args->{voicel}
29 3 100 100     229 and ref $args->{voicel} eq 'ARRAY';
30 1         5 $self->add( $args->{voicel}->@* );
31 1         10 delete $args->{voicel};
32             }
33             }
34              
35             ########################################################################
36             #
37             # METHODS
38              
39             sub add {
40 15     15 1 4619 my ( $self, @rest ) = @_;
41 15 100       138 croak "nothing to add" unless @rest;
42              
43 14         39 my $maxid = $self->voices->$#*;
44              
45 14         36 for my $ref (@rest) {
46 16 100 100     235 croak "invalid voice parameters"
47             unless defined $ref and ref $ref eq 'HASH';
48 14         29 $ref->{id} = ++$maxid;
49 14         275 push $self->voices->@*, Music::RhythmSet::Voice->new( $ref->%* );
50             }
51              
52 12         25 return $self;
53             }
54              
55             sub advance {
56 4     4 1 2856 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     25 for ( 1 .. $count // 1 ) {
64 11         29 for my $voice ( $self->voices->@* ) {
65 19         29 $param{set} = $self;
66 19         53 $voice->advance( 1, %param );
67             }
68             }
69 4         27 return $self;
70             }
71              
72             sub changes {
73 6     6 1 6158 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     431 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     29 $param{divisor} //= 1;
87 2   100     9 $param{max} //= ~0;
88              
89 2         4 my $queue = [];
90              
91 2         11 for my $voice ( $self->voices->@* ) {
92 3         11 my $beat = 0;
93 3         10 for my $ref ( $voice->replay->@* ) {
94 8         14 my ( $bpat, $ttl ) = $ref->@*;
95             # build a priority queue of when voices change their pattern
96 8         32 grpriq_add( $queue, $beat, [ $voice->id, $bpat ] );
97 8         117 $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         5 for my $entry ( $queue->@* ) { # [[id,[bp]],...],beats
106 7         55 my $measure = $entry->[1] / $param{divisor};
107 7 100       19 last if $measure >= $param{max};
108              
109 6         10 my ( @changed, @repeat );
110              
111 6         12 for my $ref ( $entry->[0]->@* ) {
112 7         15 my ( $id, $bpat ) = $ref->@*;
113 7         11 $changed[$id] = 1;
114 7         9 $curpat[$id] = $bpat;
115 7         18 my $bstr = join( '', $bpat->@* ) =~ tr/10/x./r;
116 7 100 100     27 if ( $bstr eq ( $curpat_str[$id] // '' ) ) {
117 1         2 $repeat[$id] = 1;
118             }
119 7         22 $curpat_str[$id] = $bstr;
120             }
121              
122 6         18 $param{header}->($measure);
123              
124 6         23 for my $id ( 0 .. $#curpat ) {
125 9         44 $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 473 my ($self) = @_;
136              
137 1         28 my $new = Music::RhythmSet->new;
138 1         3 my @voices;
139              
140 1         5 for my $voice ( $self->voices->@* ) {
141 2         10 push @voices, $voice->clone;
142             }
143              
144 1         6 $new->voices( \@voices );
145              
146 1         4 return $new;
147             }
148              
149             sub from_string {
150 14     14 1 4450 my ( $self, $str, %param ) = @_;
151 14 100 100     337 croak "need a string" unless defined $str and length $str;
152              
153 12   100     49 $param{rs} //= "\n";
154 12 100       26 if ( $param{sep} ) {
155 2         18 $param{sep} = qr/\Q$param{sep}\E/;
156             } else {
157 10         35 $param{sep} = qr/\s+/;
158             }
159              
160 12         22 my $linenum = 1;
161 12         16 my @newplay;
162 12         47 my $voices = $self->voices;
163              
164 12         71 for my $line ( split /\Q$param{rs}/, $str ) {
165 21 100       114 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       279 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         90 croak "ID out of range '$+{id}' at line $linenum";
187             }
188 9         123 push $newplay[ $+{id} ]->@*, [ [ split //, $+{bstr} =~ tr/x./10/r ], $+{ttl} ];
189             } else {
190 6         516 croak "invalid record at line $linenum";
191             }
192 9         36 $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       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 1438 my ( $self, $num ) = @_;
208 1         4 for my $voice ( $self->voices->@* ) {
209 2         7 $voice->measure($num);
210             }
211 1         2 return $self;
212             }
213              
214             sub to_ly {
215 2     2 1 779 my ( $self, %param ) = @_;
216              
217 2         15 for my $id ( 0 .. $self->voices->$#* ) {
218 4         8 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         6 my $id = 0;
225 2         6 return [ map { $_->to_ly( $param{voice}->[ $id++ ]->%* ) } $self->voices->@* ];
  4         21  
226             }
227              
228             sub to_midi {
229 2     2 1 665 my ( $self, %param ) = @_;
230              
231 2   100     12 $param{format} //= 1;
232 2   100     7 $param{ticks} //= 96;
233              
234 2         10 for my $id ( 0 .. $self->voices->$#* ) {
235 4         9 for my $pram (qw/chan dur maxm note notext tempo sustain velo/) {
236             $param{track}[$id]{$pram} = $param{$pram}
237 32 100 100     88 if exists $param{$pram} and not exists $param{track}[$id]{$pram};
238             }
239             }
240              
241 2         4 my $id = 0;
242             return MIDI::Opus->new(
243             { format => $param{format},
244             ticks => $param{ticks},
245             tracks =>
246 2         8 [ map { $_->to_midi( $param{track}->[ $id++ ]->%* ) } $self->voices->@* ]
  4         21  
247             }
248             );
249             }
250              
251             sub to_string {
252 2     2 1 2362 my ( $self, @rest ) = @_;
253              
254 2         4 my $str = '';
255              
256 2         8 for my $voice ( $self->voices->@* ) {
257 4         16 $str .= $voice->to_string(@rest);
258             }
259              
260 2         7 return $str;
261             }
262              
263             1;
264             __END__