File Coverage

blib/lib/Music/RhythmSet/Util.pm
Criterion Covered Total %
statement 126 126 100.0
branch 45 46 97.8
condition 35 36 97.2
subroutine 20 20 100.0
pod 14 14 100.0
total 240 242 99.1


line stmt bran cond sub pod time code
1             # -*- Perl -*-
2             #
3             # various functions related to the generation and comparison of patterns
4             # of beats, and etc
5              
6             package Music::RhythmSet::Util;
7             our $VERSION = '0.06';
8              
9 3     3   521800 use 5.24.0;
  3         10  
10 3     3   16 use warnings;
  3         5  
  3         154  
11 3     3   14 use Carp qw(croak);
  3         4  
  3         141  
12 3     3   1619 use Statistics::Lite qw(stddevp);
  3         5981  
  3         251  
13              
14 3     3   26 use constant { NOTE_ON => 1, NOTE_OFF => 0 };
  3         5  
  3         264  
15              
16 3     3   793 use parent qw(Exporter);
  3         521  
  3         107  
17             our @EXPORT_OK =
18             qw(beatstring compare_onsets duration filter_pattern flatten ocvec onset_count pattern_from rand_onsets score_fourfour score_stddev upsize write_midi write_tracks);
19              
20             sub beatstring {
21 3     3 1 160822 my ($bpat) = @_;
22 3 100 100     349 croak "no pattern set"
23             unless defined $bpat and ref $bpat eq 'ARRAY';
24 1         11 return join( '', $bpat->@* ) =~ tr/10/x./r;
25             }
26              
27             sub compare_onsets {
28 4     4 1 442 my ( $first, $second ) = @_;
29              
30 4         9 my $same = 0;
31 4         6 my $onsets = 0;
32              
33 4         18 for my $i ( 0 .. $first->$#* ) {
34 6 100       18 if ( $first->[$i] == NOTE_ON ) {
35 5         8 $onsets++;
36 5 100       14 $same++ if $second->[$i] == NOTE_ON;
37             }
38             }
39 4 100       185 croak "no onsets?! [@$first] [@$second]" unless $onsets;
40              
41 3         28 return $same / $onsets;
42             }
43              
44             sub duration {
45 3     3 1 1574 my ($replay) = @_;
46 3 100 100     308 croak "no replay log"
47             unless defined $replay and ref $replay eq 'ARRAY';
48              
49 1         3 my $measures = 0;
50 1         14 my $beats = 0;
51              
52 1         3 for my $ref ( $replay->@* ) {
53 3         6 $measures += $ref->[1];
54 3         11 $beats += $ref->[0]->@* * $ref->[1];
55             }
56              
57 1         6 return $measures, $beats;
58             }
59              
60             sub filter_pattern {
61 2     2 1 19 my ( $onsets, $total, $trials, $fudge, $nozero ) = @_;
62              
63 2   100     9 $fudge //= 0.0039;
64 2         2 my $best = ~0;
65 2         3 my $bpat;
66              
67 2         5 for ( 1 .. $trials ) {
68 11000         18035 my $new = &rand_onsets;
69 11000         19021 my $score = score_stddev($new) + score_fourfour($new) * $fudge;
70 11000 50 66     24015 next if $nozero and $score == 0;
71 11000 100       33334 if ( $score < $best ) {
72 14         23 $best = $score;
73 14         32 $bpat = $new;
74             }
75             }
76              
77 2         36 return $bpat;
78             }
79              
80             sub flatten {
81 3     3 1 1205 my ($replay) = @_;
82 3 100 100     260 croak "no replay log"
83             unless defined $replay and ref $replay eq 'ARRAY';
84 1         4 return [ map { ( $_->[0]->@* ) x $_->[1] } $replay->@* ];
  3         20  
85             }
86              
87             # "onset-coordinate vector" notation for a pattern
88             sub ocvec {
89 3     3 1 1125 my ($bpat) = @_;
90 3 100 100     252 croak "no pattern set"
91             unless defined $bpat and ref $bpat eq 'ARRAY';
92              
93 1         2 my @set;
94 1         2 my $i = 0;
95              
96 1         3 for my $x ( $bpat->@* ) {
97 12 100       21 push @set, $i if $x == NOTE_ON;
98 12         22 $i++;
99             }
100              
101 1         5 return \@set;
102             }
103              
104             sub onset_count {
105 3     3 1 1076 my ($bpat) = @_;
106 3 100 100     278 croak "no pattern set"
107             unless defined $bpat and ref $bpat eq 'ARRAY';
108              
109 1         2 my $onsets = 0;
110              
111 1         3 for my $x ( $bpat->@* ) {
112 12 100       25 $onsets++ if $x == NOTE_ON;
113             }
114              
115 1         6 return $onsets;
116             }
117              
118             sub pattern_from {
119 2     2 1 366 my ($string) = @_;
120 2         7 $string =~ tr/x.//cd;
121 2         3 $string =~ tr/x./10/;
122 2         18 return [ split '', $string ];
123             }
124              
125             sub rand_onsets {
126 11101     11101 1 24047 my ( $onsets, $total ) = @_;
127 11101 100       19766 croak "onsets must be < total" if $onsets >= $total;
128              
129 11100         13071 my @pattern;
130 11100         18426 while ($total) {
131 177000 100       268307 if ( rand() < $onsets / $total ) {
132 44500         62122 push @pattern, NOTE_ON;
133 44500         51648 $onsets--;
134             } else {
135 132500         184562 push @pattern, NOTE_OFF;
136             }
137 177000         273001 $total--;
138             }
139              
140 11100         18912 return \@pattern;
141             }
142              
143             sub score_fourfour {
144 11002     11002 1 828945 my ($bpat) = @_;
145              
146 11002         19739 my @beatquality = map { 256 - $_ } qw(
  176032         229457  
147             256 0 16 4
148             64 0 32 8
149             128 0 16 4
150             64 0 32 8
151             );
152 11002         14456 my $i = 0;
153 11002         13330 my $score = 0;
154              
155 11002         18973 for my $x ( $bpat->@* ) {
156 176016 100       266351 $score += $beatquality[$i] if $x == NOTE_ON;
157 176016         213344 $i++;
158             }
159              
160 11002         27283 return $score;
161             }
162              
163             sub score_stddev {
164 11004     11004 1 18243 my ($bpat) = @_;
165              
166 11004         12402 my @deltas;
167 11004         14515 my $len = $bpat->@*;
168              
169 11004         23055 for my $i ( 0 .. $bpat->$#* ) {
170 176048 100       285011 if ( $bpat->[$i] == NOTE_ON ) {
171 44012         55222 my $j = $i + 1;
172 44012         52911 while (1) {
173 176048 100       289960 if ( $bpat->[ $j % $len ] == NOTE_ON ) {
174 44012         55416 my $d = $j - $i;
175 44012         53541 push @deltas, $d;
176 44012         67313 last;
177             }
178 132036         151110 $j++;
179             }
180             }
181             }
182 11004 100       22133 croak "no onsets?! [@$bpat]" unless @deltas;
183              
184 11003         24288 return stddevp(@deltas);
185             }
186              
187             sub upsize {
188 7     7 1 3444 my ( $bpat, $newlen ) = @_;
189 7 100 100     588 croak "no pattern set"
      100        
190             unless defined $bpat
191             and ref $bpat eq 'ARRAY'
192             and $bpat->@*;
193 3         4 my $len = $bpat->@*;
194 3 100       275 croak "new length must be greater than pattern length"
195             if $newlen <= $len;
196 1         4 my $mul = int( $newlen / $len );
197 1         4 my @pat = (NOTE_OFF) x $newlen;
198 1         5 for my $i ( 0 .. $bpat->$#* ) {
199 4 100       11 if ( $bpat->[$i] == NOTE_ON ) {
200 3         7 $pat[ $i * $mul ] = NOTE_ON;
201             }
202             }
203 1         43 return \@pat;
204             }
205              
206             sub write_midi {
207 7     7 1 6555 my ( $file, $track, %param ) = @_;
208              
209 7   100     35 $param{format} //= 1;
210 7   100     24 $param{ticks} //= 96;
211              
212             MIDI::Opus->new(
213             { format => $param{format},
214             ticks => $param{ticks},
215 7 100       57 tracks => ref $track eq 'ARRAY' ? $track : [$track],
216             }
217             )->write_to_file($file);
218              
219 7         4427 return; # copy "write_to_file" interface
220             }
221              
222             # some DAW merge the tracks of a file into a single instrument, so one
223             # may instead need to have individual files to import
224             sub write_tracks {
225 2     2 1 1691 my ( $template, $track, %param ) = @_;
226              
227 2   100     30 $param{format} //= 1;
228 2   100     9 $param{i} //= 1;
229 2   100     8 $param{ticks} //= 96;
230              
231 2 100       10 for my $t ( ref $track eq 'ARRAY' ? @$track : $track ) {
232             MIDI::Opus->new(
233             { format => $param{format},
234             ticks => $param{ticks},
235             tracks => [$t],
236             }
237 3         483 )->write_to_file( sprintf $template, $param{i}++ );
238             }
239              
240 2         828 return;
241             }
242              
243             1;
244             __END__