File Coverage

blib/lib/Audio/Extract/PCM/Backend/SoX.pm
Criterion Covered Total %
statement 30 94 31.9
branch 2 54 3.7
condition 0 2 0.0
subroutine 10 13 76.9
pod n/a
total 42 163 25.7


line stmt bran cond sub pod time code
1             package Audio::Extract::PCM::Backend::SoX;
2 2     2   12 use strict;
  2         6  
  2         93  
3 2     2   10 use warnings;
  2         5  
  2         67  
4 2     2   13 use Carp;
  2         4  
  2         188  
5 2     2   12 use IO::CaptureOutput qw( qxx qxy );
  2         4  
  2         121  
6 2     2   14 use base qw(Audio::Extract::PCM::Backend);
  2         6  
  2         880  
7 2     2   11 use Audio::Extract::PCM::Format;
  2         4  
  2         23  
8              
9             unless (defined get_sox_version()) {
10             die __PACKAGE__ . " - trynext\n"; # try next backend
11             }
12              
13             my @bppvals = (
14             {
15             value => '-b',
16             samplesize => 1,
17             },
18             {
19             value => '-l',
20             samplesize => 4,
21             },
22             {
23             value => '-d',
24             samplesize => 8,
25             },
26             {
27             value => '-w',
28             samplesize => 2,
29             },
30             );
31             my @signvals = (
32             {
33             value => '-u',
34             signed => 0,
35             },
36             {
37             value => '-s',
38             signed => 1,
39             },
40             );
41              
42             if (get_sox_version() && get_sox_version() > '13.0.0') {
43             $_->{value} = '-' . $_->{samplesize} for @bppvals;
44             }
45              
46             =head1 NAME
47              
48             Audio::Extract::PCM::Backend::SoX - sox backend for audio extraction
49              
50             =head1 METHODS
51              
52             =head2 pcm_back
53              
54             See L.
55              
56             =head1 SYNOPSIS
57              
58             Note that this backend does not support C, i.e. it only supports the
59             C method, which will read all pcm data at once.
60              
61             =cut
62              
63             sub pcm_back {
64 0     0   0 my $this = shift;
65 0         0 my ($format) = @_;
66              
67 0 0       0 my $fn = $this->{filename} or croak 'No filename';
68              
69 0         0 my @param;
70              
71 0 0       0 if (defined $format->samplesize) {
72 0 0       0 my ($bpp_option, $bpp_format) = $format->findvalue(\@bppvals)
73             or return 'trynext';
74              
75 0         0 push @param, $bpp_option;
76             }
77             # We need either -s or -u, otherwise we cannot be sure that sox doesn't use
78             # strange output formats like u-law.
79             # We default to "-s" if no signedness is requested. Older soxes still use
80             # unsigned if the input file is more than 8-bit, newer soxes don't and
81             # convert to 8-bit instead. Cannot do anything about it, I guess. Sox
82             # doesn't have a flag that says "either signed or unsigned, I need
83             # uncompressed PCM format!"
84 0         0 my ($signoption, $signformat) = $format->findvalue(\@signvals);
85 0         0 push @param, $signoption;
86              
87 2     2   1903 use bytes;
  2         16  
  2         14  
88              
89 0         0 local $ENV{LC_ALL} = 'C';
90              
91 0 0       0 push @param, '-r'.$format->freq if defined $format->freq;
92 0 0       0 push @param, '-c'.$format->channels if defined $format->channels;
93              
94 0         0 my @command = ('sox', $fn, @param, '-twav', '-');
95              
96 0 0       0 warn qq(Running "@command"\n) if $ENV{DEBUG};
97              
98 0         0 my $pcm = \do {my $dummy};
  0         0  
99              
100 0         0 ($$pcm, my ($soxerr, $success)) = qxx(@command);
101              
102 0         0 chomp $soxerr;
103              
104             # # Well, this is ugly, but that warning is annoying and does not matter to
105             # # us (we strip the header anyway)
106             # $soxerr =~ s/.*header will be wrong since can't seek.*\s*//;
107              
108             # Now that we use -V3, we cannot display all that stderr stuff, so the
109             # above is commented out.
110             # (update: no longer true, we don't use -V3 any more)
111              
112 0 0       0 unless ($success) {
113 0         0 my $err;
114 0 0       0 if ($!) {
115 0 0       0 $err = length($soxerr) ? "$! - $soxerr" : "$!";
116             } else {
117             # show only the last line
118 0   0     0 $soxerr = [$soxerr =~ /[^\n\r]+/g]->[-1] || '';
119              
120 0 0       0 $err = length($soxerr) ? $soxerr : "Error running sox";
121             }
122              
123 0         0 undef $$pcm;
124              
125 0         0 $this->error($err);
126 0         0 return ();
127             }
128              
129             # warn $soxerr if length $soxerr;
130              
131             # Now get the format data
132              
133 0         0 my ($headersize, $endformat) = _parsewav($$pcm);
134              
135             # SoX doesn't always return what we tell it to return.
136             # Quote: "sox wav: Do not support unsigned with 16-bit data. Forcing to Signed."
137 0 0       0 return 'trynext' unless $format->satisfied($endformat);
138              
139 0         0 $this->format($endformat);
140              
141 0         0 substr($$pcm, 0, $headersize, ''); # strip wave header
142              
143 0         0 return $pcm;
144              
145             # Following is the old sox diagnostics parsing code, for historic reasons.
146              
147             # Older soxes had a very different format of the -V3 output. From sox's
148             # Changelog, I *assume* that it changed in 13.0.0.
149             # What I actually tested is 12.17.9 and 14.0.1, for sample output see the
150             # comment at the bottom of this module.
151              
152             # my ($sample_size, $duration, $channels, $endian, $endfreq, $signed);
153             #
154             # if (get_sox_version() >= '13.0.0') {
155             #
156             # my %infos = (
157             # # Parse table
158             # map {
159             # /^(.+?)\s*:\s*(.*?)\s*$/ ? (lc $1, $2) : ()
160             # }
161             # # Find output file stanza
162             # grep {
163             # /^Output File\s*:/i .. /^\s*$/
164             # }
165             # # Split lines
166             # $soxerr =~ /[^\n\r]+/g
167             # );
168             #
169             # if ($ENV{DEBUG}) {
170             # warn "SoX stderr:\n$soxerr\n";
171             # }
172             #
173             # $channels = $infos{channels} or die "no channels from sox";
174             # $endian = $infos{'endian type'} or die "no endianness from sox";
175             # $endfreq = $infos{'sample rate'} or die "no sample rate from sox";
176             #
177             # $sample_size = $infos{'sample size'} || $infos{precision}
178             # or die "no sample size from sox";
179             # $sample_size =~ s/^([0-9]+).*// or die "bad sample size from sox: $sample_size";
180             # $sample_size = $1 / 8;
181             #
182             # $duration = $infos{duration} or die "no duration from sox";
183             # $duration =~ / (\d+) samples/i or die "bad duration from sox: $duration";
184             # $duration = $1 / $endfreq;
185             #
186             # my $encoding = $infos{'sample encoding'};
187             # $signed = $encoding =~ /\bsigned\b/i ? 1 : $encoding =~ /\bunsigned\b/i ? 0
188             # : die "no signed from sox";
189             #
190             # } else {
191             # # sox < 13.0.0
192             #
193             # my ($info) = $soxerr =~ m{^(sox: Writing Wave file:.*?bits/samp)}msi;
194             # my ($info2) = $soxerr =~ m{^(sox: Output file .*? channels\s*$)}msi;
195             #
196             # ($sample_size) = $info =~ m{(\d+) bits/samp}i or die "no sample size from sox";
197             # $sample_size /= 8;
198             #
199             # ($endfreq) = $info =~ m{(\d+) samp/sec}i or die "no sample rate from sox";
200             # $endian = 'little'; # ?
201             # ($channels) = $info =~ m{(\d+) channels}i or die "no channels from sox";
202             #
203             # ($duration) = $soxerr =~ m{^sox: Finished writing.*?(\d+) samples}mi
204             # or die "no duration from sox";
205             # $duration /= $endfreq;
206             #
207             # $signed = $info2 =~ /encoding signed/i ? 1 : $info2 =~ /encoding unsigned/i ? 0
208             # : die "no signed from sox: $info2";
209             # }
210             }
211              
212              
213              
214             =head1 SUBROUTINES
215              
216             =head2 get_sox_version
217              
218             This will return the SoX version as a L object. The result will be
219             cached, i.e. if you install a new sox, this module will not recognize it until
220             it is reloaded or the application using it is restarted.
221              
222             If no sox program is in the path or C outputs strange things, C
223             will be returned. In the latter case, a warning will be issued.
224              
225             Currently, the only internal use is to find out whether SoX is above version
226             13.0.0, because they renamed the C<"-b"/"-w"/"-l"/"-d"> flags to
227             C<"-1"/"-2"/"-4"/"-8"> by then. Note that the old flags were still recognized
228             (though deprecated) until SoX 14.1.0.
229              
230             =cut
231              
232             {
233             my $soxver;
234              
235             sub get_sox_version {
236 2 50   2   13 return $soxver if defined $soxver;
237              
238             # Note: we use sox -h, not sox --version.
239             # The latter doesn't work with e.g. 12.17.9 (etch)
240             # Older soxes print stuff like "sox: Version 12.17.9" in the first line of sox -h,
241             # newer soxes print stuff like "sox: SoX v14.0.1" instead.
242              
243 2         13 my ($vers_output, $success, $exitstatus) = qxy(qw(sox -h));
244              
245             # return undef unless $success;
246             # argh, old soxes have exit status 1 for sox -h (and print to stderr;
247             # rather than stdout like newer ones)
248 2     2   2197 use POSIX qw(WIFEXITED);
  2         8484  
  2         18  
249 2 50       15900 return undef unless WIFEXITED($exitstatus);
250              
251 0 0         if (defined $vers_output) {
252 0           $vers_output =~ s#[\r\n].*##s;
253              
254 0 0         ($soxver) = $vers_output =~ /(?:Version |v)(\d+\.\d+\.\d+)/
255             or warn "Strange sox -h output (first line): $vers_output\n";
256 0 0         return undef unless defined $soxver;
257             }
258              
259 2     2   3703 use version;
  2         5429  
  2         14  
260 0           $soxver = version->new($soxver);
261              
262 0           return $soxver;
263             }
264             }
265              
266              
267             =head2 used_versions
268              
269             Abstract interface to L.
270              
271             =cut
272              
273             sub used_versions {
274             return {
275 0     0     sox => get_sox_version(),
276             };
277             }
278              
279              
280             # This analyzes the header of the wave files that sox outputs.
281              
282             # I used to parse the diagnostics output from sox -V3, but when I realized how
283             # different it is in different sox versions, I found that it's easier to just
284             # parse the wave header that sox produces. This is a very simple wave header
285             # analyzer; it is designed only for the headers from sox with the flags that we
286             # give to it. There are more sophisticated modules like Audio::Wav for other
287             # wave files.
288              
289             # By the way, why don't I use sox' "raw" format? Well, for one thing, I'd
290             # have to specify exact format parameters to sox, while for wave output, it
291             # will default to the input format, which seems wiser. I think there was
292             # another reason, but I can't remember right now...
293             sub _parsewav {
294 0     0     my ($header) = @_;
295              
296 0           my $headersize = 44;
297 0           my $datachunkstart = 36;
298              
299             # parse riff header
300 0           my ($RIFF, $riffsize, $WAVE) = unpack ('a4Va4', $header);
301 0 0         die 'no riff' unless 'RIFF' eq $RIFF;
302 0 0         die 'no wave' unless 'WAVE' eq $WAVE;
303              
304             # parse format header
305 0           my ($fmt, $compr, $chans, $freq, $bps) = unpack (
306             '@12a4x4vvVx6v', $header);
307              
308 0 0         die 'no fmt' unless 'fmt ' eq $fmt;
309              
310 0 0         if (0xFFFE == $compr) {
311             # WAVE_FORMAT_EXTENSIBLE
312              
313 0           (my ($extsize), $compr, my ($strange_magic_thing)) =
314             unpack('@36vx6vH28', $header);
315              
316 0 0         unless ('000000001000800000AA00389B71' eq uc $strange_magic_thing) {
317 0           die 'unexpected strange magic thing';
318             }
319 0 0         die "unexpected extsize $extsize" unless 22 == $extsize;
320              
321 0           $_ += 24 for $headersize, $datachunkstart;
322             }
323              
324 0 0         die 'only PCM/uncompressed supported' unless 1 == $compr;
325              
326 0           my $samplesize = $bps / 8;
327 0 0         die 'only multiples of 8bps supported' unless $samplesize == int $samplesize;
328              
329             {
330 0           my ($chunkname, $chunksize) = unpack("\@${datachunkstart}a4V", $header);
  0            
331              
332 0 0         if ('fact' eq $chunkname) {
333 0           $_ += 12 for $headersize, $datachunkstart;
334 0           redo;
335             }
336              
337 0 0         die 'no "data"' unless 'data' eq $chunkname;
338              
339             # Alright, we're in the data chunk at last.
340              
341 0           my $duration = $chunksize / $chans / $samplesize / $freq;
342              
343 0 0         return $headersize, AEPF->new(
344             channels => $chans,
345             freq => $freq,
346             duration => $duration,
347             samplesize => $samplesize,
348             signed => ($samplesize > 1 ? 1 : 0),
349             endian => 'little',
350             );
351             }
352             }
353              
354              
355             our $AVAILABLE = 1;
356              
357             # Sample -V3 output from sox:
358              
359             # 12.17.9:
360             #
361             # sox: invalid option -- 3
362             # (hehe, but as long as it isn't an error, why not)
363             # sox: Detected file format type: ogg
364             #
365             # sox: Input file t/sine.ogg: using sample rate 44100
366             # size shorts, encoding Vorbis, 2 channels
367             # sox: Do not support Vorbis with 16-bit data. Forcing to Signed.
368             # sox: Writing Wave file: Microsoft PCM format, 2 channels, 44100 samp/sec
369             # sox: 176400 byte/sec, 4 block align, 16 bits/samp
370             # sox: Output file sine.wav: using sample rate 44100
371             # size shorts, encoding signed (2's complement), 2 channels
372             # sox: Output file: comment "Processed by SoX"
373             #
374             # sox: Finished writing Wave file, 1764000 data bytes 441000 samples
375             #
376             # and 14.0.1:
377             # sox: SoX v14.0.1
378             #
379             # Input File : 't/sine.ogg'
380             # Sample Size : 16-bit (2 bytes)
381             # Sample Encoding: Vorbis
382             # Channels : 2
383             # Sample Rate : 44100
384             # Duration : 00:10.00 = 441000 samples = 750 CDDA sectors
385             # Endian Type : little
386             # Reverse Nibbles: no
387             # Reverse Bits : no
388             #
389             # sox wav: Do not support Vorbis with 16-bit data. Forcing to Signed.
390             #
391             # Output File : 'sine.wav'
392             # Sample Size : 16-bit (2 bytes)
393             # Sample Encoding: signed (2's complement)
394             # Channels : 2
395             # Sample Rate : 44100
396             # Duration : 00:10.00 = 441000 samples = 750 CDDA sectors
397             # Endian Type : little
398             # Reverse Nibbles: no
399             # Reverse Bits : no
400             # Comment : 'Processed by SoX'
401             #
402             # sox sox: effects chain: input 44100Hz 2 channels 16 bits (multi)
403             # sox sox: effects chain: output 44100Hz 2 channels 16 bits (multi)
404             #
405             #
406             # now here comes 14.2.0:
407             #
408             # sox: SoX v14.2.0
409             # sox formats: detected file format type `wav'
410             #
411             # Input File : 'sine.wav'
412             # Channels : 2
413             # Sample Rate : 44100
414             # Precision : 16-bit
415             # Duration : 00:00:10.00 = 441000 samples = 750 CDDA sectors
416             # Sample Encoding: 16-bit Signed Integer PCM
417             # Endian Type : little
418             # Reverse Nibbles: no
419             # Reverse Bits : no
420             #
421             #
422             # Output File : '-' (wav)
423             # Channels : 2
424             # Sample Rate : 44100
425             # Precision : 16-bit
426             # Duration : 00:00:10.00 = 441000 samples = 750 CDDA sectors
427             # Sample Encoding: 16-bit Signed Integer PCM
428             # Endian Type : little
429             # Reverse Nibbles: no
430             # Reverse Bits : no
431             # Comment : 'Processed by SoX'
432             #
433             # sox sox: effects chain: input 44100Hz 2 channels 16 bits (multi)
434             # sox sox: effects chain: output 44100Hz 2 channels 16 bits (multi)
435              
436              
437             =head1 SEE ALSO
438              
439             =over 8
440              
441             =item *
442              
443             L - SoX homepage
444              
445             =back