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