File Coverage

blib/lib/Music/RhythmSet/Util.pm
Criterion Covered Total %
statement 119 119 100.0
branch 44 44 100.0
condition 30 30 100.0
subroutine 19 19 100.0
pod 13 13 100.0
total 225 225 100.0


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.05';
8              
9 3     3   494912 use 5.24.0;
  3         21  
10 3     3   18 use warnings;
  3         7  
  3         84  
11 3     3   16 use Carp qw(croak);
  3         5  
  3         195  
12 3     3   1453 use Statistics::Lite qw(stddevp);
  3         4655  
  3         210  
13              
14 3     3   22 use constant { NOTE_ON => 1, NOTE_OFF => 0 };
  3         12  
  3         269  
15              
16 3     3   1447 use parent qw(Exporter);
  3         879  
  3         18  
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);
19              
20             sub beatstring {
21 3     3 1 7102 my ($bpat) = @_;
22 3 100 100     314 croak "no pattern set"
23             unless defined $bpat and ref $bpat eq 'ARRAY';
24 1         9 return join( '', $bpat->@* ) =~ tr/10/x./r;
25             }
26              
27             sub compare_onsets {
28 4     4 1 486 my ( $first, $second ) = @_;
29              
30 4         6 my $same = 0;
31 4         7 my $onsets = 0;
32              
33 4         15 for my $i ( 0 .. $first->$#* ) {
34 6 100       16 if ( $first->[$i] == NOTE_ON ) {
35 5         9 $onsets++;
36 5 100       16 $same++ if $second->[$i] == NOTE_ON;
37             }
38             }
39 4 100       104 croak "no onsets?! [@$first] [@$second]" unless $onsets;
40              
41 3         25 return $same / $onsets;
42             }
43              
44             sub duration {
45 3     3 1 1353 my ($replay) = @_;
46 3 100 100     197 croak "no replay log"
47             unless defined $replay and ref $replay eq 'ARRAY';
48              
49 1         4 my $measures = 0;
50 1         2 my $beats = 0;
51              
52 1         5 for my $ref ( $replay->@* ) {
53 3         5 $measures += $ref->[1];
54 3         8 $beats += $ref->[0]->@* * $ref->[1];
55             }
56              
57 1         7 return $measures, $beats;
58             }
59              
60             sub filter_pattern {
61 2     2 1 25 my ( $onsets, $total, $trials, $fudge, $nozero ) = @_;
62              
63 2   100     10 $fudge //= 0.0039;
64 2         4 my $best = ~0;
65 2         5 my $bpat;
66              
67 2         6 for ( 1 .. $trials ) {
68 11000         17536 my $new = &rand_onsets;
69 11000         18091 my $score = score_stddev($new) + score_fourfour($new) * $fudge;
70 11000 100 100     22142 next if $nozero and $score == 0;
71 10997 100       26327 if ( $score < $best ) {
72 12         22 $best = $score;
73 12         26 $bpat = $new;
74             }
75             }
76              
77 2         26 return $bpat;
78             }
79              
80             sub flatten {
81 3     3 1 847 my ($replay) = @_;
82 3 100 100     181 croak "no replay log"
83             unless defined $replay and ref $replay eq 'ARRAY';
84 1         3 return [ map { ( $_->[0]->@* ) x $_->[1] } $replay->@* ];
  3         17  
85             }
86              
87             # "onset-coordinate vector" notation for a pattern
88             sub ocvec {
89 3     3 1 878 my ($bpat) = @_;
90 3 100 100     190 croak "no pattern set"
91             unless defined $bpat and ref $bpat eq 'ARRAY';
92              
93 1         12 my @set;
94 1         4 my $i = 0;
95              
96 1         3 for my $x ( $bpat->@* ) {
97 12 100       26 push @set, $i if $x == NOTE_ON;
98 12         15 $i++;
99             }
100              
101 1         6 return \@set;
102             }
103              
104             sub onset_count {
105 3     3 1 901 my ($bpat) = @_;
106 3 100 100     183 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       24 $onsets++ if $x == NOTE_ON;
113             }
114              
115 1         4 return $onsets;
116             }
117              
118             sub pattern_from {
119 2     2 1 495 my ($string) = @_;
120 2         6 $string =~ tr/x.//cd;
121 2         4 $string =~ tr/x./10/;
122 2         21 return [ split '', $string ];
123             }
124              
125             sub rand_onsets {
126 11101     11101 1 23139 my ( $onsets, $total ) = @_;
127 11101 100       19114 croak "onsets must be < total" if $onsets >= $total;
128              
129 11100         13742 my @pattern;
130 11100         18060 while ($total) {
131 177000 100       287667 if ( rand() < $onsets / $total ) {
132 44500         60861 push @pattern, NOTE_ON;
133 44500         54929 $onsets--;
134             } else {
135 132500         179466 push @pattern, NOTE_OFF;
136             }
137 177000         274365 $total--;
138             }
139              
140 11100         18184 return \@pattern;
141             }
142              
143             sub score_fourfour {
144 11002     11002 1 883118 my ($bpat) = @_;
145              
146 11002         18801 my @beatquality = map { 256 - $_ } qw(
  176032         237197  
147             256 0 16 4
148             64 0 32 8
149             128 0 16 4
150             64 0 32 8
151             );
152 11002         15638 my $i = 0;
153 11002         14469 my $score = 0;
154              
155 11002         16574 for my $x ( $bpat->@* ) {
156 176016 100       270581 $score += $beatquality[$i] if $x == NOTE_ON;
157 176016         226073 $i++;
158             }
159              
160 11002         23800 return $score;
161             }
162              
163             sub score_stddev {
164 11004     11004 1 18751 my ($bpat) = @_;
165              
166 11004         13705 my @deltas;
167 11004         14852 my $len = $bpat->@*;
168              
169 11004         19859 for my $i ( 0 .. $bpat->$#* ) {
170 176048 100       290462 if ( $bpat->[$i] == NOTE_ON ) {
171 44012         55796 my $j = $i + 1;
172 44012         54194 while (1) {
173 176048 100       292970 if ( $bpat->[ $j % $len ] == NOTE_ON ) {
174 44012         59118 my $d = $j - $i;
175 44012         56516 push @deltas, $d;
176 44012         64190 last;
177             }
178 132036         158200 $j++;
179             }
180             }
181             }
182 11004 100       20235 croak "no onsets?! [@$bpat]" unless @deltas;
183              
184 11003         22184 return stddevp(@deltas);
185             }
186              
187             sub upsize {
188 7     7 1 2637 my ( $bpat, $newlen ) = @_;
189 7 100 100     417 croak "no pattern set"
      100        
190             unless defined $bpat
191             and ref $bpat eq 'ARRAY'
192             and $bpat->@*;
193 3         6 my $len = $bpat->@*;
194 3 100       218 croak "new length must be greater than pattern length" if $newlen <= $len;
195 1         4 my $mul = int( $newlen / $len );
196 1         4 my @pat = (NOTE_OFF) x $newlen;
197 1         4 for my $i ( 0 .. $bpat->$#* ) {
198 4 100       11 if ( $bpat->[$i] == NOTE_ON ) {
199 3         6 $pat[ $i * $mul ] = NOTE_ON;
200             }
201             }
202 1         12 return \@pat;
203             }
204              
205             sub write_midi {
206 7     7 1 7824 my ( $file, $track, %param ) = @_;
207              
208 7   100     41 $param{format} //= 1;
209 7   100     30 $param{ticks} //= 96;
210              
211             MIDI::Opus->new(
212             { format => $param{format},
213             ticks => $param{ticks},
214 7 100       58 tracks => ref $track eq 'ARRAY' ? $track : [$track]
215             }
216             )->write_to_file($file);
217              
218 7         4032 return; # copy "write_to_file" interface
219             }
220              
221             1;
222             __END__