File Coverage

blib/lib/Audio/Extract/PCM/Format.pm
Criterion Covered Total %
statement 85 116 73.2
branch 36 64 56.2
condition 5 9 55.5
subroutine 12 15 80.0
pod 4 4 100.0
total 142 208 68.2


line stmt bran cond sub pod time code
1             package Audio::Extract::PCM::Format;
2 4     4   688 use strict;
  4         12  
  4         286  
3 4     4   27 use warnings;
  4         9  
  4         161  
4 4     4   24 use Carp;
  4         8  
  4         962  
5 4     4   28 use base qw(Class::Accessor::Fast);
  4         14  
  4         4601  
6 4     4   33116 use List::Util qw(max);
  4         10  
  4         583  
7 4     4   4422 use List::MoreUtils qw(any);
  4         7327  
  4         425  
8 4     4   36 use base qw(Exporter);
  4         10  
  4         409  
9              
10 4     4   23 use constant AEPF => __PACKAGE__;
  4         10  
  4         13138  
11             our @EXPORT = qw(AEPF);
12              
13             our @CARP_NOT = qw(Audio::Extract::PCM);
14              
15              
16             my %valid = (
17             freq => qr{^[0-9]+\z},
18             samplesize => qr{^[0-9]+\z},
19             endian => qr{^(?:little|big)\z},
20             channels => qr{^[1-9]\z}, # empirical
21             signed => qr{^[01]\z},
22             duration => qr{^[0-9]+(?:\.[0-9]*)?\z},
23             );
24              
25             my @fields = keys %valid;
26              
27             __PACKAGE__->mk_accessors(@fields, 'required');
28              
29             my $localendian = '10002000' eq unpack('h*', pack('s2', 1, 2))
30             ? 'little'
31             : 'big';
32              
33             =head1 NAME
34              
35             Audio::Extract::PCM::Format - Format of PCM data
36              
37             =head1 SYNOPSIS
38              
39             This class is used by L and its backends to represent the
40             format of PCM data.
41              
42             =head1 ACCESSORS
43              
44             =over 8
45              
46             =item freq
47              
48             Also known as the sample rate, in samples per second.
49              
50             =item samplesize
51              
52             In bytes per sample.
53              
54             =item endian
55              
56             The string C or C. The constructor also accepts the string
57             C.
58              
59             This will print your native endianness ("little" or "big"):
60              
61             print Audio::Extract::PCM::Format->new(endian => 'native')->endian;
62              
63             I've read somewhere that there are computers that have "middle endianness", and
64             maybe there are computers that don't use any of the three. I only support
65             systems with either little or big endianness.
66              
67             =item channels
68              
69             A number.
70              
71             =item signed
72              
73             1 or 0, which means signed or unsigned, respectively.
74              
75             =item duration
76              
77             (seconds, may be fractional)
78              
79             Of course, it doesn't make sense to specify the duration when you call
80             L, however it will return you an object that has a
81             duration field, but it may be undefined if the backend does not support getting
82             the duration.
83              
84             Once you have extracted all the pcm data, you can get the duration in seconds
85             using the formula:
86              
87             pcm_buffer_length / samplesize / channels / freq
88              
89             =back
90              
91             =head1 METHODS
92              
93             =head2 new
94              
95             Constructor. You'll probably call this when you want to call
96             L or L. In this case, the
97             following semantics apply:
98              
99             Specify required values for frequency (samples per second), samplesize
100             (bytes per sample), channels, endianness and signedness:
101              
102             Audio::Extract::PCM::Format->new(
103             freq => 44100,
104             samplesize => 2,
105             channels => 2,
106             endian => 'native',
107             signed => 1,
108             );
109              
110             If you omit a specification (or it is "undef"), the value will be chosen by the
111             back-end.
112              
113             Additionally, there are some special ways to say what you want:
114              
115             Audio::Extract::PCM::Format->new(
116              
117             # The frequency *must* be one of 44100, 48000
118             freq => [44100, 48000],
119              
120             # If *possible*, you would like little endian, but you accept other
121             # values too (aka "nice-to-have" values):
122             endian => \['little'],
123             );
124              
125              
126             Finally, there is a short form:
127              
128             Audio::Extract::PCM::Format->new($freq, $samplesize, $channels);
129              
130             This is equivalent to:
131              
132             Audio::Extract::PCM::Format->new(
133             freq => $freq,
134             samplesize => $samplesize,
135             channels => $channels,
136             endian => 'native',
137             );
138              
139             =cut
140              
141             sub new {
142 9     9 1 153171 my $class = shift;
143              
144 9 50       36 my %args = (3 == @_ ? (
145             freq => $_[0],
146             samplesize => $_[1],
147             channels => $_[2],
148             endian => 'native',
149             ) : @_);
150              
151 9         40 my $this = $class->SUPER::new();
152              
153 9         78 my %required;
154              
155 9         16 for my $field (@fields) {
156 54         131 my $spec = delete $args{$field};
157              
158 54 100       110 if (defined $spec) {
159              
160 15 100       39 if ('ARRAY' eq ref $spec) {
    100          
161 3 50       8 croak "$field has no values (try undef)" unless @$spec;
162              
163 3         6 for (@$spec) {
164              
165 6 50 33     14 if ('endian' eq $field && 'native' eq $_) {
166 0         0 $_ = $localendian;
167             }
168              
169 6 50       17 unless ($valid{$field}) {
170 0         0 croak "Not a valid $field: $_";
171             }
172             }
173              
174 3         7 $required{$field} = [@$spec];
175 3         5 $spec = $spec->[0];
176              
177             } elsif ('REF' eq ref $spec) {
178              
179 5 50       13 croak "bad argument for $field" unless 'ARRAY' eq ref $$spec;
180 5 50       12 croak "(currently) only one argument is supported for nice-to-have" unless 1 == @$$spec;
181              
182 5         5 $spec = ${$spec}->[0];
  5         9  
183              
184             } else {
185              
186 7         12 $required{$field} = 1;
187             }
188              
189 15 50 66     49 if ('endian' eq $field && 'native' eq $spec) {
190 0         0 $spec = $localendian;
191             }
192              
193 15 50       89 unless ($spec =~ $valid{$field}) {
194 0         0 croak "Not a valid $field: $spec";
195             }
196              
197 15         49 $this->$field($spec);
198             }
199             }
200              
201 9 50       34 if (keys %args) {
202 0         0 croak 'Unknown argument(s): ' . join '/', keys %args;
203             }
204              
205 9         23 $this->required(\%required);
206              
207 9         50 return $this;
208             }
209              
210              
211             =head2 findvalue
212              
213             This is a useful method if you want to write your own backend. You give it a
214             list of the formats that your backend can provide, and it tells you which one
215             fits the user's wishes best (according to the rules described under L).
216              
217             See the source of the provided backends for how to use it.
218              
219             =cut
220              
221             sub findvalue {
222 10     10 1 212 my $this = shift;
223 10         14 my ($values) = @_;
224              
225 10         13 my %scores;
226              
227 10         16 VAL: for (@$values) {
228 29         99 my $score = 0;
229              
230 29         40 for my $field (@fields) {
231 159         211 my $provided = $_->{$field};
232 159         368 my $wanted = $this->$field();
233              
234 159 100       717 next unless defined $provided;
235              
236 80 50       357 unless ($provided =~ $valid{$field}) {
237 0         0 confess "Probably a badly-written backend ($provided is not a valid $field)";
238             }
239              
240 80 100       205 if (defined $wanted) {
    100          
241              
242 33         54 my @wanted = ($wanted);
243 33 100       72 @wanted = @{$this->required->{$field}} if 'ARRAY' eq ref $this->required->{$field};
  9         51  
244              
245 33 100   41   310 if (any {_equal($_, $provided)} @wanted) {
  41         87  
246              
247             # Increment the score for values we want.
248              
249 17         62 $score += 10;
250              
251             } else {
252              
253             # Don't return values that differ from required
254             # characteristics
255 16 100       49 next VAL if $this->required->{$field};
256              
257             # See the test "choosing the smaller evil" in the test
258             # suite for the reason why we decrement so much here
259 6         43 $score -= 1000;
260             }
261             }
262              
263             elsif ('endian' eq $field) {
264              
265             # If no particular endian-ness is requested, we score the local
266             # endianness a little higher.
267              
268 4 50       13 if ($provided ne $localendian) {
269 0         0 $score--;
270             }
271             }
272             }
273              
274 19 50       43 if ($ENV{DEBUG}) {
275 0         0 warn "Scoring $score for " . $_->{value} . "\n";
276             }
277              
278 19         50 $scores{$score} = $_;
279             }
280              
281 10 100       56 return undef unless keys %scores;
282              
283 9         50 my $maxscore = max keys %scores;
284 9         14 my $found= $scores{$maxscore};
285              
286 9 100       58 return $found->{value} unless wantarray;
287              
288 1         1 my %specs;
289 1         2 @specs{@fields} = @{$found}{@fields};
  1         6  
290              
291 1         4 my $foundformat = __PACKAGE__->new(%specs);
292              
293 1         6 return ($found->{value}, $foundformat);
294             }
295              
296              
297             # Compares numbers or strings
298             sub _equal {
299 41     41   49 my ($x, $y) = @_;
300              
301 41         106 my $number_re = qr(^[0-9]+\z);
302              
303 41 100 66     304 if ($x =~ $number_re && $y =~ $number_re) {
304 37         128 return $x == $y;
305             } else {
306 4         14 return $x eq $y;
307             }
308             }
309              
310              
311              
312             =head2 combine
313              
314             Argument: another format object
315              
316             Combines the values of two format objects. Modifies C<$this> and returns it.
317              
318             =cut
319              
320             sub combine {
321 0     0 1   my $this = shift;
322 0           my ($other) = @_;
323              
324 0 0         if (@_ != 1) {
325 0           $other = __PACKAGE__->new(@_);
326             }
327              
328 0           for my $field (@fields) {
329 0 0         if (defined $other->$field()) {
330 0           $this->$field($other->$field());
331 0           $this->required->{$field} = $other->required->{$field};
332             }
333             }
334              
335 0           return $this;
336             }
337              
338              
339             =head2 satisfied
340              
341             Argument: another format object
342              
343             If more than one argument is given, the arguments will be interpreted like
344             those of L.
345              
346             Returs whether the other format satisfies all I properties of this
347             object.
348              
349             =cut
350              
351             sub satisfied {
352 0     0 1   my $this = shift;
353 0           my ($other) = @_;
354              
355 0 0         if (@_ > 1) {
356 0           $other = __PACKAGE__->new(@_);
357             }
358              
359 0 0         confess 'no other format given' unless ref $other;
360              
361 0           for my $field (@fields) {
362 0 0         next unless $this->required->{$field};
363 0 0         next unless defined $other->$field();
364              
365 0           my @required = $this->$field();
366 0 0         @required = @{$this->required->{$field}} if 'ARRAY' eq ref $this->required->{$field};
  0            
367              
368 0 0   0     return () unless any {_equal($other->$field(), $_)} @required;
  0            
369             }
370              
371 0           return 1;
372             }
373              
374              
375             =head1 EXPORTS
376              
377             This package exports the constant "AEPF", which expands to the name of this module.
378              
379             =cut
380              
381              
382             1