File Coverage

blib/lib/Audio/Nama/EffectChain.pm
Criterion Covered Total %
statement 35 206 16.9
branch 0 50 0.0
condition 0 22 0.0
subroutine 12 36 33.3
pod 0 15 0.0
total 47 329 14.2


line stmt bran cond sub pod time code
1             # ------------- Effect-Chain and -Profile routines --------
2             # Effect Chains
3             #
4             # we have two type of effect chains
5             # + global effect chains - usually user defined, available to all projects
6             # + system generated effect chains, per project
7              
8             {
9             package Audio::Nama::EffectChain;
10 1     1   6 use Modern::Perl;
  1         1  
  1         7  
11 1     1   112 use Data::Dumper::Concise;
  1         2  
  1         61  
12 1     1   5 use Carp;
  1         2  
  1         59  
13 1     1   5 use Exporter qw(import);
  1         2  
  1         88  
14 1     1   12 use Storable qw(dclone);
  1         2  
  1         57  
15 1     1   6 use Audio::Nama::Effect qw(fxn append_effect);
  1         2  
  1         50  
16 1     1   5 use Audio::Nama::Log qw(logpkg logsub);
  1         2  
  1         50  
17 1     1   5 use Audio::Nama::Assign qw(json_out);
  1         2  
  1         49  
18              
19 1     1   5 use Audio::Nama::Globals qw($fx_cache %tn $fx);
  1         2  
  1         172  
20              
21             our $AUTOLOAD;
22             our $VERSION = 0.001;
23 1     1   6 no warnings qw(uninitialized);
  1         2  
  1         87  
24             our @ISA;
25             our ($n, %by_index, @attributes, %is_attribute);
26 1         7 use Audio::Nama::Object qw(
27              
28             n
29             ops_list
30             ops_data
31             inserts_data
32             region
33             attrib
34             class
35              
36 1     1   5 );
  1         3  
37             @attributes = qw(
38             name
39             bypass
40             id
41             project
42             global
43             profile
44             user
45             system
46             track_name
47             track_version_result
48             track_version_original
49             track_target_original
50             insert
51             track_cache
52             track_target
53             ) ;
54              
55             ### attributes for searching, sorting, used by external functions
56             # name # for user-defined effect chains
57             #
58             # bypass # used for identifying effect bypass (obsolete)
59             # id # effect id, for storing single effect with controllers
60             # # for bypass (probably obsolete)
61             #
62             # project # true value identifies project-specific effect chain
63             #
64             # global # true value identified global effect chain,
65             # # not project specific, usually user-defined
66             #
67             # profile # name of associated effect profile
68             #
69             # user # true value identifies user created effect chain
70             #
71             # system # true value identifies system generated effect chain
72             #
73             # track_name # applies to a track of this name
74             #
75             # track_version_result # WAV version of track after caching
76             #
77             # track_version_original # WAV version of track before caching
78             #
79             # insert # true value identifies belonging to an insert
80             #
81             # track_cache # true value identifies belonging to track caching
82             #
83             # track_target_original # WAV files were from this track
84              
85             %is_attribute = map{ $_ => 1 } @attributes;
86             initialize();
87              
88             ## sugar for accessing individual effect attributes
89             ## similar sugar is used for effects.
90              
91             sub is_controller {
92 0     0 0 0 my ($self, $id) = @_;
93             $self->{ops_data}->{$id}->{belongs_to}
94 0         0 }
95             sub parent_id : lvalue {
96 0     0 0 0 my ($self, $id) = @_;
97             $self->{ops_data}->{$id}->{belongs_to}
98 0         0 }
99             sub type {
100 0     0 0 0 my ($self, $id) = @_;
101             $self->{ops_data}->{$id}->{type}
102 0         0 }
103             sub params {
104 0     0 0 0 my ($self, $id) = @_;
105             $self->{ops_data}->{$id}->{params}
106 0         0 }
107              
108             sub initialize {
109 1     1 0 3 $n = 0;
110 1         3 %by_index = ();
111             }
112 0 0   0 0   sub new_index { $n++; $by_index{$n} ? new_index() : $n }
  0            
113             sub new {
114             # arguments: ops_list, ops_data, inserts_data
115             # ops_list => [id1, id2, id3,...];
116 0     0 0   my $class = shift;
117 0           my %vals = @_;
118              
119             # we need to so some preparation if we are creating
120             # an effect chain for the first time (as opposed
121             # to restoring a serialized effect chain)
122              
123 0 0         if (! $vals{n} ) {
124              
125             # move secondary attributes to $self->{attrib}->{...}
126 0           move_attributes(\%vals);
127              
128 0           $vals{n} = new_index();
129 0   0       $vals{inserts_data} ||= [];
130 0   0       $vals{ops_list} ||= [];
131 0   0       $vals{ops_data} ||= {};
132 0 0         croak "undeclared field in: @_" if grep{ ! $_is_field{$_} } keys %vals;
  0            
133             croak "must have exactly one of 'global' or 'project' fields defined"
134 0 0 0       unless ($vals{attrib}{global} xor $vals{attrib}{project});
135              
136 0     0     logpkg(__FILE__,__LINE__,'debug','constructor arguments ', sub{ json_out(\%vals) });
  0            
137              
138             # we expect some effects
139             logpkg(__FILE__,__LINE__,'warn',"Nether ops_list or nor insert_data is present")
140 0 0 0       if ! scalar @{$vals{ops_list}} and ! scalar @{$vals{inserts_data}};
  0            
  0            
141              
142 0           my $ops_data = {};
143             # ops data is taken preferentially
144             # from ops_data argument, with fallback
145             # to existing effects
146            
147             # in both cases, we clone the data structures
148             # to ensure we don't damage the original
149            
150             map {
151              
152 0 0         if ( $vals{ops_data}->{$_} )
153            
154             {
155 0           $ops_data->{$_} = dclone($vals{ops_data}->{$_});
156             }
157             else
158             {
159 0           my $filtered_op_data = dclone( fxn($_)->as_hash );# copy
160 0           my @unwanted_keys = qw( chain bypassed name surname display);
161 0           delete $filtered_op_data->{$_} for @unwanted_keys;
162 0           $ops_data->{$_} = $filtered_op_data;
163             }
164              
165 0           } @{$vals{ops_list}};
  0            
166            
167              
168 0           $vals{ops_data} = $ops_data;
169              
170 0 0         if( scalar @{$vals{inserts_data}})
  0            
171             {
172              
173             # rewrite inserts to store what we need:
174             # 1. for general-purpose effects chain use
175             # 2. for track caching use
176            
177            
178             $vals{inserts_data} =
179             [
180             map
181             {
182 0     0     logpkg(__FILE__,__LINE__,'debug',"insert: ", sub{Dumper $_});
  0            
183 0           my @wet_ops = @{$tn{$_->wet_name}->ops};
  0            
184 0           my @dry_ops = @{$tn{$_->dry_name}->ops};
  0            
185 0           my $wet_effect_chain = Audio::Nama::EffectChain->new(
186             project => 1,
187             insert => 1,
188             ops_list => \@wet_ops,
189             );
190 0           my $dry_effect_chain = Audio::Nama::EffectChain->new(
191             project => 1,
192             insert => 1,
193             ops_list => \@dry_ops,
194             );
195 0           my $hash = dclone($_->as_hash);
196              
197 0           $hash->{wet_effect_chain} = $wet_effect_chain->n;
198 0           $hash->{dry_effect_chain} = $dry_effect_chain->n;
199              
200 0           map{ delete $hash->{$_} } qw(n dry_vol wet_vol track);
  0            
201              
202             # Reasons for deleting insert attributes
203            
204             # n: we'll get a new index when we re-apply
205             # dry_vol, wet_vol: will never be re-allocated
206             # so why not reuse them?
207             # except for general purpose we'd like to
208             # re-allocate
209             # track: we already know the track from
210             # the parent effect chain
211              
212             # What is left:
213             #
214             # class
215             # wetness
216             # send_type
217             # send_id
218             # return_type
219             # return_id
220             # wet_effect_chain => ec_index,
221             # dry_effect_chain => ec_index,
222            
223 0           $hash
224 0           } @{$vals{inserts_data}}
  0            
225             ];
226             }
227              
228             #say Audio::Nama::json_out($vals{inserts_data}) if $vals{inserts_data};
229             }
230 0           my $object = bless { %vals }, $class;
231 0           $by_index{$vals{n}} = $object;
232 0     0     logpkg(__FILE__,__LINE__,'debug',sub{$object->dump});
  0            
233 0           $object;
234             }
235             sub AUTOLOAD {
236 0     0     my $self = shift;
237 0           my ($call) = $AUTOLOAD =~ /([^:]+)$/;
238             return $self->{attrib}->{$call} if exists $self->{attrib}->{$call}
239 0 0 0       or $is_attribute{$call};
240 0           croak "Autoload fell through. Object type: ", (ref $self), ", illegal method call: $call\n";
241             }
242              
243             ### apply effect chain to the specified track
244              
245             sub add_ops {
246 0     0 0   my($self, $track, $ec_args) = @_;
247              
248             # Higher priority: track argument
249             # Lower priority: effect chain's own track name attribute
250 0 0 0       $track ||= $tn{$self->track_name} if $tn{$self->track_name};
251            
252              
253             # make sure surname is unique
254            
255 0           my ($new_surname, $existing) = $track->unique_surname($ec_args->{surname});
256 0 0         if ( $new_surname ne $ec_args->{surname})
257             {
258 0           Audio::Nama::pager_newline(
259             "track ".
260             $track->name.qq(: other effects with surname "$ec_args->{surname}" found,),
261             qq(using "$new_surname". Others are: $existing.));
262 0           $ec_args->{surname} = $new_surname;
263             }
264              
265            
266 0           logpkg(__FILE__,__LINE__,'debug',$track->name,
267             qq(: adding effect chain ), $self->name, Dumper $self
268            
269             );
270              
271             # Exclude restoring vol/pan for track_caching.
272             # (This conditional is a hack that would be better
273             # implemented by subclassing EffectChain
274             # for cache/uncache)
275            
276 0           my @ops_list;
277             my @added;
278 0 0         if( $self->track_cache ){
279 0 0         @ops_list = grep{ $_ ne $track->vol and $_ ne $track->pan }
280 0           @{$self->ops_list}
  0            
281             } else {
282 0           @ops_list = @{$self->ops_list};
  0            
283             }
284             map
285             {
286 0           my $args =
  0            
287             {
288             chain => $track->n,
289             type => $self->type($_),
290             params => $self->params($_),
291             parent => $self->parent_id($_),
292             };
293              
294            
295             # drop the ID if it is already used
296 0 0         $args->{id} = $_ unless fxn($_);
297              
298 0           logpkg(__FILE__,__LINE__,'debug',"args ", json_out($args));
299              
300 0 0         $args->{surname} = $ec_args->{surname} if $ec_args->{surname};
301              
302 0           my $FX = append_effect($args)->[0];
303 0           push @added, $FX;
304 0           my $new_id = $FX->id;
305            
306             # the effect ID may be new, or it may be previously
307             # assigned ID,
308             # whatever value is supplied is guaranteed
309             # to be unique; not to collide with any other effect
310            
311 0           logpkg(__FILE__,__LINE__,'debug',"new id: $new_id");
312 0           my $orig_id = $_;
313 0 0         if ( $new_id ne $orig_id)
314             # re-write all controllers to belong to new id
315             {
316 0           map{ $self->parent_id($_) =~ s/^$orig_id$/$new_id/ } @{$self->ops_list}
  0            
  0            
317             }
318            
319             } @ops_list;
320             \@added
321 0           }
322             sub add_inserts {
323 0     0 0   my ($self, $track) = @_;
324             map
325             {
326 0           my $insert_data = dclone($_); # copy so safe to modify
327             #say "found insert data:\n",Audio::Nama::json_out($insert_data);
328              
329             # get effect chain indices for wet/dry arms
330            
331 0           my $wet_effect_chain = delete $insert_data->{wet_effect_chain};
332 0           my $dry_effect_chain = delete $insert_data->{dry_effect_chain};
333 0           my $class = delete $insert_data->{class};
334              
335 0           $insert_data->{track} = $track->name;
336 0           my $insert = $class->new(%$insert_data);
337             #$Audio::Nama::by_index{$wet_effect_chain}->add($insert->wet_name, $tn{$insert->wet_name}->vol)
338             #$Audio::Nama::by_index{$dry_effect_chain}->add($insert->dry_name, $tn{$insert->dry_name}->vol)
339 0           } @{$self->inserts_data};
  0            
340             }
341             sub add_region {
342 0     0 0   my ($self, $track) = @_;
343             # there is also a check in uncache track
344 0 0         Audio::Nama::throw($track->name.": track already has region definition\n",
345             "failed to apply region @$self->{region}\n"), return
346             if $track->is_region;
347             $track->set(region_start => $self->{region}->[0],
348 0           region_end => $self->{region}->[1]);
349             }
350              
351             sub add {
352 0     0 0   my ($self, $track, $successor) = @_;
353             # TODO stop_do_start should take place at this level
354             # possibly reconfiguring engine
355 0           my $args = {};
356 0           $args->{before} = $successor;
357 0 0         $args->{surname} = $self->name if $self->name;
358 0           my $added = $self->add_ops($track, $args);
359 0           $self->add_inserts($track);
360 0 0         $self->add_region($track) if $self->region;
361 0           $added
362              
363             }
364             sub destroy {
365 0     0 0   my $self = shift;
366 0           delete $by_index{$self->n};
367             }
368              
369             #### class routines
370            
371             sub find {
372              
373             # find(): search for an effect chain by attributes
374             #
375             # Returns EffectChain objects in list context,
376             # number of matches in scalar context.
377              
378 0     0 0   my %args = @_;
379 0           my $unique = delete $args{unique};
380              
381             # first check for a specified index that matches
382             # an existing chain
383            
384 0 0         return $by_index{$args{n}} if $args{n};
385              
386             # otherwise all specified fields must match
387            
388             my @found = grep
389 0           { my $fx_chain = $_;
  0            
390            
391             # check if any specified fields *don't* match
392            
393             my @non_matches = grep
394             {
395              
396 0           ! ($fx_chain->{attrib}->{$_} eq $args{$_})
  0            
397              
398             #! ($_ ne 'version' and $args{$_} eq 1 and $fx_chain->$_)
399              
400             } keys %args;
401              
402             # if no non-matches, then all have matched,
403             # and we return true
404              
405             ! scalar @non_matches
406            
407 0           } values %by_index;
408              
409 0 0 0       warn("unique chain requested but multiple chains found. Skipping.\n"),
410             return if $unique and @found > 1;
411              
412 0 0         if( wantarray() ){ $unique ? pop @found : sort{ $a->n cmp $b->n } @found }
  0 0          
  0            
413 0           else { scalar @found }
414             }
415              
416             sub summary {
417 0     0 0   my $self = shift;
418 0           my @output;
419 0 0         push @output, " name: ".$self->name if $self->name;
420 0 0         push @output, " track name: ".$self->track_name if $self->track_name;
421             push @output,
422             map{
423 0           my $i = Audio::Nama::effect_index( $self->{ops_data}->{$_}->{type} );
424 0           my $name = " ". $fx_cache->{registry}->[$i]->{name};
425 0           } @{$_->ops_list};
  0            
426 0           map{ $_,"\n"} @output;
  0            
427             }
428              
429             sub move_attributes {
430 0     0 0   my $ec_hash = shift;
431 0           map { $ec_hash->{attrib}->{$_} = delete $ec_hash->{$_} }
432 0           grep{ $ec_hash->{$_} }
  0            
433             @attributes;
434             }
435              
436       0     sub DESTROY {}
437              
438             }
439             {
440             #### Effect-chain and -profile routines
441              
442             package Audio::Nama;
443             sub add_effect_chain {
444 0     0     my ($name, $track, $successor) = @_;
445 0           my ($ec) = Audio::Nama::EffectChain::find(
446             unique => 1,
447             user => 1,
448             name => $name,
449             );
450 0 0         if( $ec ){ $ec->add($Audio::Nama::this_track, $successor) }
  0            
451 0           else { Audio::Nama::throw("$name: effect chain not found") }
452 0           1;
453             }
454             sub new_effect_profile {
455 0     0     logsub("&new_effect_profile");
456 0           my ($bunch, $profile) = @_;
457 0           my @tracks = bunch_tracks($bunch);
458 0           Audio::Nama::pager( qq(effect profile "$profile" created for tracks: @tracks) );
459             map {
460 0           Audio::Nama::EffectChain->new(
461             profile => $profile,
462             user => 1,
463             global => 1,
464             track_name => $_,
465             ops_list => [ $tn{$_}->fancy_ops ],
466 0           inserts_data => $tn{$_}->inserts,
467             );
468             } @tracks;
469             }
470             sub delete_effect_profile {
471 0     0     logsub("&delete_effect_profile");
472 0           my $name = shift;
473 0           Audio::Nama::pager( qq(deleting effect profile: $name) );
474 0           map{ $_->destroy} Audio::Nama::EffectChain::find( profile => $name );
  0            
475             }
476              
477             sub apply_effect_profile { # overwriting current effects
478 0     0     logsub("&apply_effect_profile");
479 0           my ($profile) = @_;
480 0           my @chains = Audio::Nama::EffectChain::find(profile => $profile);
481              
482             # add missing tracks
483 0           map{ Audio::Nama::pager( "adding track $_" ); add_track($_) }
  0            
484 0           grep{ !$tn{$_} }
485 0           map{ $_->track_name } @chains;
  0            
486             # add effect chains
487 0           map{ $_->add } @chains;
  0            
488             }
489             sub is_effect_chain {
490 0     0     my $name = shift;
491 0           my ($fxc) = Audio::Nama::EffectChain::find(name => $name, unique => 1);
492 0           $fxc
493             }
494             }
495             1;
496             __END__