File Coverage

blib/lib/Math/SegmentedEnvelope.pm
Criterion Covered Total %
statement 167 180 92.7
branch 78 92 84.7
condition 34 57 59.6
subroutine 33 43 76.7
pod 13 30 43.3
total 325 402 80.8


line stmt bran cond sub pod time code
1             package Math::SegmentedEnvelope;
2             # ABSTRACT: create/manage/evaluate segmented (curved) envelope
3 1     1   4472 use Moo;
  1         62404  
  1         9  
4 1     1   6272 use Clone 'clone';
  1         5668  
  1         97  
5 1     1   11 use Carp;
  1         2  
  1         86  
6 1     1   6 use List::Util 'sum';
  1         2  
  1         164  
7 1     1   7 use constant PI => 4 * atan2(1, 1);
  1         2  
  1         77  
8 1     1   1555 use Exporter::Easy (OK => ['env']);
  1         2913  
  1         12  
9 1     1   1634 use namespace::autoclean;
  1         36604  
  1         9  
10              
11             has def => ( is => 'ro', default => sub { # random by default
12             my $size = int rand(5) + 3;
13             my $level = shift->border_level;
14             [
15             [$level->[0], map(rand, (0) x $size), $level->[1]],
16             [normalize_sum(map rand() + 0.2, (0) x ($size + 1))],
17             [map { (rand(2) + 1) * (int(rand(2))? 1 : -1) } (0) x ($size + 1)]
18             ];
19             });
20             has border_level => is => rw => default => sub { # default border level for start and end
21             [ (rand)x2 ]
22             } => coerce => sub {
23             ref($_[0]) eq 'ARRAY' ? $_[0] : [($_[0])x2];
24             };
25             has is_morph => ( is => 'rw' );
26             has morpher => ( is => 'rw', default => sub { sub { sin( $_[0] * PI / 2 ) ** 2 } } );
27             has is_hold => ( is => 'rw' );
28             has is_fold_over => ( is => 'rw' );
29             has is_wrap_neg => ( is => 'rw' );
30              
31             has _duration => ( is => 'rw' );
32             has _segments => ( is => 'rw' );
33             has _current_segment => ( is => 'rw', default => sub { 0 } );
34             has _level_diff => ( is => 'rw' );
35             has _is_neg => ( is => 'rw' );
36             has _is_asc => ( is => 'rw' );
37             has _past_segment => ( is => 'rw', default => sub { -1 } );
38             has _passed_segments_duration => ( is => 'rw', default => sub { 0 } );
39              
40 0     0 1 0 sub env { __PACKAGE__->new(@_) }
41              
42             sub BUILDARGS {
43 2     2 0 40438 my ( $class, @args ) = @_;
44 2 100       15 unshift @args, "def" if @args % 2 == 1;
45 2         54 return { @args };
46             };
47              
48             sub BUILD {
49 2     2 0 16 my ($self) = @_;
50 2         12 croak "size mismatch in envelope definition" if
51 2         14 @{$self->def->[0]} != @{$self->def->[1]} + 1
  2         6  
52 2         14 || @{$self->def->[0]} != @{$self->def->[2]} + 1
  2         6  
53 2 50 33     3 || @{$self->def->[1]} != @{$self->def->[2]};
  2   33     13  
54 2         5 $self->_duration(sum(@{$self->def->[1]}));
  2         21  
55 2         4 $self->_segments(scalar@{$self->def->[1]});
  2         55  
56             }
57              
58             sub clean {
59 3     3 0 77 my ($self) = @_;
60 3         4 $self->_duration(sum(@{$self->def->[1]}));
  3         20  
61 3         3 $self->_segments(scalar@{$self->def->[1]});
  3         10  
62 3         55 $self->_current_segment(0);
63 3         7 $self->_past_segment(-1);
64             }
65              
66             sub evaluator {
67 0     0 0 0 my ($self) = @_;
68 0     0   0 sub { $self->at(@_) };
  0         0  
69             }
70              
71             sub at {
72 22     22 1 73 my ($self, $t) = @_;
73 22         51 $t = $self->wrap_pos($t);
74 22         78 my ($pd,$i,$d) = (
75             $self->_passed_segments_duration,
76             $self->_current_segment
77             );
78 22   66     297 while ($t < $pd && $i > 0) { $pd -= $self->def->[1]->[--$i] } # backward
  15         71  
79 22 100       276 $i == 0 ? $pd = 0 : $t -= $pd; # remove duration of passed segments
80 22         68 while ($i < $self->_segments) { # forward - determine segment and cache it for next time
81 41         146 $d = $self->def->[1]->[$i]; # set current segment duration + error
82 41 100 66     146 if ($t > $d && $i != $self->_segments - 1) { # t passed this segment, so remove this segment duration
83 19         22 $t -= $d; $pd += $d; $i++; next;
  19         22  
  19         19  
  19         87  
84             } else { # $t is in current segment
85 22 50       52 $t = $d if $t > $d;
86 22 100       85 $i = $self->update_current_segment($i) unless $i == $self->_past_segment; last;
  22         37  
87             }
88             }
89             # print "r:$i\tt:$t\td:$d\tp:$pd";
90 22 100       71 $self->_passed_segments_duration($pd) if $pd != $self->_passed_segments_duration;
91 22 50       53 $self->_current_segment($i) if $i != $self->_current_segment;
92             abs( # result value
93 22 50       186 $self->wrap_value(abs(( $self->_is_neg ? $d - $t : $t ) / $d))
94             ** abs($self->def->[2]->[$i])
95             * $self->_is_asc
96             + $self->_is_neg
97             ) * $self->_level_diff + $self->def->[0]->[$i];
98             #print "\t$t\n"; $t;
99             }
100              
101             sub wrap_value {
102 22     22 0 36 my ($self) = @_;
103 22 100       3281 $self->is_morph ? $self->morpher->($_[1]) : $_[1]; # value smooth or whatever
104             }
105              
106             sub wrap_pos {
107 22     22 0 71 my ($self,$t) = @_;
108 22         45 my $total = $self->_duration;
109 22 100       82 if ($self->is_hold) {
110 10 100       44 $t > 0 ? ( $t > $total ? $total : $t ) : 0
    100          
111             } else {
112 12         20 my $at = abs($t);
113 12 100       29 if ($at > $total) {
114 4 100 100     43 if ($self->is_fold_over && int($at/$total) % 2 == ( $t < 0 && $self->is_wrap_neg ? 0 : 1 )) { #fold
    100 100        
115 2         8 ( 1 - ( ($at / $total) - int($at / $total) ) ) * $total;
116             } else { # wrap
117 2         8 ( ($at / $total) - int($at / $total) ) * $total;
118             }
119 8         16 } else { $at }
120             };
121             }
122              
123             sub update_current_segment {
124 12     12 0 241 my ($self, $i) = @_;
125 12 100       47 $i = $self->_current_segment(defined($i) ? $i : ());
126 12         45 $self->_level_diff($self->level($i+1) - $self->level($i));
127 12 50       46 $self->_is_neg($self->curve($i) < 0 ? 1 : 0);
128 12 100 66     72 $self->_is_asc($self->_level_diff < 0 || $self->_is_neg ? -1 : 1);
129 12         30 $self->_past_segment($i);
130             }
131              
132             sub level {
133 27     27 1 121 my $self = shift;
134 27         61 my $r = $self->def_part_value(0, @_);
135 27 50 100     84 $self->update_current_segment if @_ > 1 && abs($self->_current_segment - ($_[0] >= 0 ? $_[0] : $self->_segments + $_[0])) <= 1;
    100          
136 27         72 $r;
137             }
138              
139             sub levels {
140 6     6 1 10 my $self = shift;
141 6         32 my @r = $self->def_part(0, @_);
142 6 100       22 $self->update_current_segment if @_ > 0;
143 6         41 @r;
144             }
145              
146             sub dur {
147 1     1 1 2 my $self = shift;
148 1         4 my $r = $self->def_part_value(1, @_);
149 1 50       6 $self->clean if @_ > 1;
150 1         2 $r;
151             }
152              
153             sub durs {
154 8     8 1 13 my $self = shift;
155 8         34 my @r = $self->def_part(1, @_);
156 8 100       3220 $self->clean if @_ > 1;
157 8         64 @r;
158             }
159              
160 23     23 1 264 sub duration { shift->_duration }
161 4     4 1 32 sub segments { shift->_segments }
162              
163             sub curve {
164 13     13 1 94 my $self = shift;
165 13         40 my $r = $self->def_part_value(2, @_);
166 13 100 66     62 $self->update_current_segment if @_ > 1 && $self->_current_segment == $_[0];
167 13         268 $r;
168             }
169              
170             sub curves {
171 6     6 1 82 my $self = shift;
172 6         22 my @r = $self->def_part(2, @_);
173 6 100       21 $self->update_current_segment if @_ > 0;
174 6         98 @r;
175             }
176              
177             sub def_part {
178 20     20 0 39 my ($self, $p, @values) = @_;
179 20 50       51 (@values == @{$self->def->[$p]} ? $self->def->[$p] = [@values] : carp "size mismatch against initial definition") if @values;
  4 100       33  
180 20         78 @{$self->def->[$p]};
  20         121  
181             }
182              
183             sub def_part_value {
184 41     41 0 386 my ($self, $p, $at, $value) = @_;
185 41 50 33     273 croak "no such index '$at' in definition part '$p'" if !defined($at) || !exists($self->def->[$p]->[$at]);
186 41 100       156 $self->def->[$p]->[$at] = $value if $value;
187 41         110 $self->def->[$p]->[$at];
188             }
189              
190             sub static { # make immutable evaluator from current params
191 4     4 1 79 my ($self) = @_;
192 4   66     22 my ($lev, $dur, $cur, $is_smooth, $is_hold, $is_fold_over, $is_wrap_neg, $total) = (
193             [$self->levels], [$self->durs], [$self->curves], $self->is_morph && clone($self->morpher),
194             $self->is_hold, $self->is_fold_over, $self->is_wrap_neg, $self->duration
195             );
196 4         15 my ($i, $pd, $cs, $level_diff, $is_asc, $is_neg, $d) = (0, 0, -1); # segment index and its data
197             my $segment_data = sub {
198 10     10   31 $level_diff = $lev->[$i+1] - $lev->[$i];
199 10 100       31 $is_neg = $cur->[$i] < 0 ? 1 : 0;
200 10 100 66     40 $is_asc = $level_diff < 0 || $is_neg ? -1 : 1;
201 10         18 $cs = $i;
202 4         35 };
203 4 50   13   27 my $wrap_value = $is_smooth ? ( ref($is_smooth) eq 'CODE' ? $is_smooth : sub { sin( PI / 2 * $_[0] ) } ) : sub { $_[0] }; # value smooth or whatever
  0 100       0  
  13         111  
204             my $wrap_pos = $is_hold ? sub {
205 10 100   10   43 $_[0] > 0 ? ( $_[0] > $total ? $total : $_[0] ) : 0;
    100          
206             } : sub {
207 1028     1028   1673 my $t = abs($_[0]);
208 1028 100       2096 if ($t > $total) { #fold
209 1 50 33     19 if ($is_fold_over && int($t/$total) % 2 == ($_[0] < 0 && $is_wrap_neg ? 0 : 1)) {
    50 33        
210 0         0 (1 - (($t / $total) - int( $t / $total ))) * $total;
211             } else { # wrap
212 1         6 (($t / $total) - int( $t / $total )) * $total;
213             }
214 1027         2200 } else { $t }
215 4 100       249 };
216 4         14 my $last_segment = @$dur - 1;
217             sub {
218 1038     1038   2320 my $t = $wrap_pos->($_[0]);
219 1038   66     6110 while ($t < $pd && $i > 0) { $pd -= $dur->[--$i] } # backward
  7         33  
220 1038 100       2496 $i == 0 ? $pd = 0 : $t -= $pd; # remove duration of passed segments
221 1038         2771 while ($i <= $last_segment) { # forward - determine segment and cache it for next tiem
222 1059         1619 $d = $dur->[$i]; # set current segment duration
223 1059 100 66     3657 if ($t > $d && $i != $last_segment) { # t passed this segment, so remove this segment duration
224 21         25 $t -= $d; $pd += $d; $i++; next;
  21         28  
  21         23  
  21         44  
225             } else { # $t is in current segment
226 1038 50       7594 $t = $d if $t > $d;
227 1038 100       2166 $segment_data->() unless $i == $cs; last;
  1038         1623  
228             }
229             }
230             #print "r:$i\tt:$t\td:$d\tp:$pd";
231             abs( # result value
232 1038 100       2666 $wrap_value->(( $is_neg ? ($d - $t) : $t ) / $d)
233             ** abs($cur->[$i])
234             * $is_asc
235             + $is_neg
236             ) * $level_diff + $lev->[$i];
237             #print "\t$t\n"; $t;
238             }
239 4         655 }
240              
241             sub table { # create lookup table of specified size, loops and range
242 1     1 1 7 my ($self, $size, $loop, $from, $to) = @_;
243 1   50     4 $size ||= 1024;
244 1   50     7 $loop ||= 1;
245 1   50     5 $from ||= 0;
246 1   33     6 $to ||= $self->duration;
247 1 50       4 croak "table size should be >= 1" if $size <= 0;
248 1         79 my $s = $self->static;
249 1         2 my $range = $to - $from;
250 1         3 my $lp = $loop / $size;
251 1         2 my $p;
252 1024         1507 map {
253 1         75 $p = $_ * $lp;
254 1024         2619 $s->($from + $range * ($p - int $p));
255             } 0..$size-1;
256             }
257              
258             sub normalize_duration {
259 1     1 1 8 my ($self) = @_;
260 1         5 $self->durs(normalize_sum($self->durs));
261 1         3 $self;
262             }
263              
264             sub normalize_sum {
265 2     2 0 92 my $s = sum@_;
266 2         16 map $_/$s, @_;
267             }
268              
269             # TODO utility methods
270 0     0 0   sub stack {} # concat?
271 0     0 0   sub blend {}
272 0     0 0   sub delay {}
273             # TODO some usual envelopes
274 0     0 0   sub adsr {}
275 0     0 0   sub asr {}
276 0     0 0   sub cutoff {}
277 0     0 0   sub perc {}
278              
279             1;