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.03';
8              
9 3     3   476448 use 5.24.0;
  3         21  
10 3     3   18 use warnings;
  3         7  
  3         102  
11 3     3   18 use Carp qw(croak);
  3         5  
  3         146  
12 3     3   1554 use Statistics::Lite qw(stddevp);
  3         4765  
  3         206  
13              
14 3     3   24 use constant { NOTE_ON => 1, NOTE_OFF => 0 };
  3         12  
  3         251  
15              
16 3     3   1518 use parent qw(Exporter);
  3         839  
  3         70  
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 7657 my ($bpat) = @_;
22 3 100 100     374 croak "no pattern set"
23             unless defined $bpat and ref $bpat eq 'ARRAY';
24 1         10 return join( '', $bpat->@* ) =~ tr/10/x./r;
25             }
26              
27             sub compare_onsets {
28 4     4 1 488 my ( $first, $second ) = @_;
29              
30 4         7 my $same = 0;
31 4         7 my $onsets = 0;
32              
33 4         14 for my $i ( 0 .. $first->$#* ) {
34 6 100       19 if ( $first->[$i] == NOTE_ON ) {
35 5         8 $onsets++;
36 5 100       12 $same++ if $second->[$i] == NOTE_ON;
37             }
38             }
39 4 100       111 croak "no onsets?! [@$first] [@$second]" unless $onsets;
40              
41 3         32 return $same / $onsets;
42             }
43              
44             sub duration {
45 3     3 1 1357 my ($replay) = @_;
46 3 100 100     202 croak "no replay log"
47             unless defined $replay and ref $replay eq 'ARRAY';
48              
49 1         3 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         17 $beats += $ref->[0]->@* * $ref->[1];
55             }
56              
57 1         7 return $measures, $beats;
58             }
59              
60             sub filter_pattern {
61 2     2 1 26 my ( $onsets, $total, $trials, $fudge, $nozero ) = @_;
62              
63 2   100     11 $fudge //= 0.0039;
64 2         3 my $best = ~0;
65 2         6 my $bpat;
66              
67 2         7 for ( 1 .. $trials ) {
68 11000         18043 my $new = &rand_onsets;
69 11000         19179 my $score = score_stddev($new) + score_fourfour($new) * $fudge;
70 11000 100 100     23370 next if $nozero and $score == 0;
71 10998 100       27435 if ( $score < $best ) {
72 15         22 $best = $score;
73 15         35 $bpat = $new;
74             }
75             }
76              
77 2         33 return $bpat;
78             }
79              
80             sub flatten {
81 3     3 1 915 my ($replay) = @_;
82 3 100 100     205 croak "no replay log"
83             unless defined $replay and ref $replay eq 'ARRAY';
84 1         4 return [ map { ( $_->[0]->@* ) x $_->[1] } $replay->@* ];
  3         25  
85             }
86              
87             # "onset-coordinate vector" notation for a pattern
88             sub ocvec {
89 3     3 1 882 my ($bpat) = @_;
90 3 100 100     198 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       31 push @set, $i if $x == NOTE_ON;
98 12         20 $i++;
99             }
100              
101 1         7 return \@set;
102             }
103              
104             sub onset_count {
105 3     3 1 853 my ($bpat) = @_;
106 3 100 100     193 croak "no pattern set"
107             unless defined $bpat and ref $bpat eq 'ARRAY';
108              
109 1         3 my $onsets = 0;
110              
111 1         3 for my $x ( $bpat->@* ) {
112 12 100       23 $onsets++ if $x == NOTE_ON;
113             }
114              
115 1         8 return $onsets;
116             }
117              
118             sub pattern_from {
119 2     2 1 554 my ($string) = @_;
120 2         7 $string =~ tr/x.//cd;
121 2         6 $string =~ tr/x./10/;
122 2         24 return [ split '', $string ];
123             }
124              
125             sub rand_onsets {
126 11101     11101 1 23363 my ( $onsets, $total ) = @_;
127 11101 100       19355 croak "onsets must be < total" if $onsets >= $total;
128              
129 11100         14045 my @pattern;
130 11100         18790 while ($total) {
131 177000 100       290773 if ( rand() < $onsets / $total ) {
132 44500         62133 push @pattern, NOTE_ON;
133 44500         58073 $onsets--;
134             } else {
135 132500         187988 push @pattern, NOTE_OFF;
136             }
137 177000         278983 $total--;
138             }
139              
140 11100         19251 return \@pattern;
141             }
142              
143             sub score_fourfour {
144 11002     11002 1 895424 my ($bpat) = @_;
145              
146 11002         17879 my @beatquality = map { 256 - $_ } qw(
  176032         236590  
147             256 0 16 4
148             64 0 32 8
149             128 0 16 4
150             64 0 32 8
151             );
152 11002         14902 my $i = 0;
153 11002         14769 my $score = 0;
154              
155 11002         17920 for my $x ( $bpat->@* ) {
156 176016 100       279520 $score += $beatquality[$i] if $x == NOTE_ON;
157 176016         228878 $i++;
158             }
159              
160 11002         24930 return $score;
161             }
162              
163             sub score_stddev {
164 11004     11004 1 18670 my ($bpat) = @_;
165              
166 11004         13429 my @deltas;
167 11004         15012 my $len = $bpat->@*;
168              
169 11004         20413 for my $i ( 0 .. $bpat->$#* ) {
170 176048 100       295938 if ( $bpat->[$i] == NOTE_ON ) {
171 44012         56062 my $j = $i + 1;
172 44012         55131 while (1) {
173 176048 100       285173 if ( $bpat->[ $j % $len ] == NOTE_ON ) {
174 44012         57692 my $d = $j - $i;
175 44012         60652 push @deltas, $d;
176 44012         67271 last;
177             }
178 132036         157761 $j++;
179             }
180             }
181             }
182 11004 100       20464 croak "no onsets?! [@$bpat]" unless @deltas;
183              
184 11003         23332 return stddevp(@deltas);
185             }
186              
187             sub upsize {
188 7     7 1 2740 my ( $bpat, $newlen ) = @_;
189 7 100 100     427 croak "no pattern set"
      100        
190             unless defined $bpat
191             and ref $bpat eq 'ARRAY'
192             and $bpat->@*;
193 3         7 my $len = $bpat->@*;
194 3 100       190 croak "new length must be greater than pattern length" if $newlen <= $len;
195 1         4 my $mul = int( $newlen / $len );
196 1         5 my @pat = (NOTE_OFF) x $newlen;
197 1         6 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         7 return \@pat;
203             }
204              
205             sub write_midi {
206 7     7 1 7795 my ( $file, $track, %param ) = @_;
207              
208 7   100     38 $param{format} //= 1;
209 7   100     27 $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         3962 return; # copy "write_to_file" interface
219             }
220              
221             1;
222             __END__