File Coverage

blib/lib/Audio/Wav/Write.pm
Criterion Covered Total %
statement 146 168 86.9
branch 40 62 64.5
condition 5 9 55.5
subroutine 19 22 86.3
pod 10 11 90.9
total 220 272 80.8


line stmt bran cond sub pod time code
1             package Audio::Wav::Write;
2              
3 1     1   8 use strict;
  1         2  
  1         70  
4             eval { require warnings; }; #it's ok if we can't load warnings
5              
6 1     1   1082 use FileHandle;
  1         14954  
  1         7  
7 1     1   1584 use Audio::Wav::Write::Header;
  1         3  
  1         43  
8              
9 1     1   6 use vars qw( $VERSION );
  1         2  
  1         848  
10             $VERSION = '0.14';
11              
12             =head1 NAME
13              
14             Audio::Wav::Write - Module for writing Microsoft WAV files.
15              
16             =head1 SYNOPSIS
17              
18             use Audio::Wav;
19              
20             my $wav = new Audio::Wav;
21              
22             my $sample_rate = 44100;
23             my $bits_sample = 16;
24              
25             my $details = {
26             'bits_sample' => $bits_sample,
27             'sample_rate' => $sample_rate,
28             'channels' => 1,
29             # if you'd like this module not to use a write cache, uncomment the next line
30             #'no_cache' => 1,
31              
32             };
33              
34             my $write = $wav -> write( 'testout.wav', $details );
35              
36             &add_sine( 200, 1 );
37              
38             sub add_sine {
39             my $hz = shift;
40             my $length = shift;
41             my $pi = ( 22 / 7 ) * 2;
42             $length *= $sample_rate;
43             my $max_no = ( 2 ** $bits_sample ) / 2 - 1;
44             for my $pos ( 0 .. $length ) {
45             $time = $pos / $sample_rate;
46             $time *= $hz;
47             my $val = sin $pi * $time;
48             my $samp = $val * $max_no;
49             $write -> write( $samp );
50             }
51             }
52              
53             $write -> finish();
54              
55             =head1 DESCRIPTION
56              
57             Currently only writes to a file.
58              
59             =head1 SEE ALSO
60              
61             L
62              
63             L
64              
65             =head1 NOTES
66              
67             This module shouldn't be used directly, a blessed object can be returned from L.
68              
69             =head1 METHODS
70              
71             =cut
72              
73             sub new {
74 2     2 0 4 my $class = shift;
75 2         22 my $out_file = shift;
76 2         3 my $details = shift;
77 2         4 my $tools = shift;
78              
79 2 50       20 my $handle = (ref $out_file eq 'GLOB') ? $out_file : new FileHandle ">$out_file";
80              
81 2         429 my $use_cache = 1;
82 2 50 33     22 if ( ref $details eq 'HASH' && exists $details -> {no_cache} ) {
83 0         0 my $no_cache = delete $details -> {no_cache};
84 0 0       0 $use_cache = 0 if $no_cache;
85             }
86              
87 2         20 my $self = {
88             'use_cache' => $use_cache,
89             'write_cache' => undef,
90             'out_file' => $out_file,
91             'cache_size' => 4096,
92             'handle' => $handle,
93             'details' => $details,
94             'block_align' => $details -> {block_align},
95             'tools' => $tools,
96             'done_finish' => 0,
97             };
98              
99 2         7 bless $self, $class;
100              
101 2 50       6 unless ( defined $handle ) {
102 0         0 my $error = $!;
103 0         0 chomp $error;
104 0         0 $self -> _error( "unable to open file ($error)" );
105 0         0 return $self;
106             }
107              
108 2         6 binmode $handle;
109              
110 2         7 $self -> _init();
111 2         8 $self -> _start_file();
112 2         7 $self -> _examine_details( $details );
113              
114 2 50       7 if ( $self -> {details} -> {bits_sample} <= 8 ) {
115 2         8 $self -> {use_offset} = ( 2 ** $self -> {details} -> {bits_sample} ) / 2;
116             } else {
117 0         0 $self -> {use_offset} = 0;
118             }
119              
120 2         7 return $self;
121             }
122              
123             sub DESTROY {
124 1     1   13 my $self = shift;
125 1 50       4 return unless $self;
126 1 50       27 return if $self -> {done_finish};
127 0         0 $self -> finish();
128             }
129              
130             =head2 finish
131              
132             Finishes off & closes the current wav file.
133              
134             $write -> finish();
135              
136             =cut
137              
138             sub finish {
139 2     2 1 15 my $self = shift;
140 2 50       11 $self -> _purge_cache() if $self -> {use_cache};
141 2         13 $self -> {header} -> finish( $self -> {pos} );
142 2         13 $self -> {handle} -> close();
143 2         68 $self -> {done_finish} = 1;
144             }
145              
146             =head2 add_cue
147              
148             Adds a cue point to the wav file. If $sample is undefined then the position will be the current position (end of all data written so far).
149              
150             # $byte_offset for 01 compatibility mode
151             $write -> add_cue( $sample, "label", "note" );
152              
153             =cut
154              
155             sub add_cue {
156 6     6 1 169 my $self = shift;
157 6         9 my $pos = shift;
158 6         8 my $label = shift;
159 6         8 my $note = shift;
160 6         15 my $block_align = $self -> {details} -> {block_align};
161 6 50       15 if ( defined $pos ) {
162 6 50       21 $pos /= $block_align if $self -> {tools} -> is_01compatible();
163             } else {
164 0         0 $pos = $self -> {pos} / $block_align;
165             }
166 6         37 my $output = {
167             'pos' => $pos,
168             };
169 6 50       22 $output -> {label} = $label if $label;
170 6 50       16 $output -> {note} = $note if $note;
171 6         24 $self -> {header} -> add_cue( $output );
172             }
173              
174             =head2 set_sampler_info
175              
176             All parameters are optional.
177              
178             my %info = (
179             'midi_pitch_fraction' => 0,
180             'smpte_format' => 0,
181             'smpte_offset' => 0,
182             'product' => 0,
183             'sample_period' => 0,
184             'manufacturer' => 0,
185             'sample_data' => 0,
186             'midi_unity_note' => 65,
187             );
188             $write -> set_sampler_info( %info );
189              
190             =cut
191              
192             sub set_sampler_info {
193 1     1 1 5 my ($self, @args) = @_;
194 1         5 return $self -> {header} -> set_sampler_info( @args );
195             }
196              
197             =head2 add_sampler_loop
198              
199             All parameters are optional except start & end.
200              
201             my $length = $read -> length_samples();
202             my( $third, $twothirds ) = map int( $length / $_ ), ( 3, 1.5 );
203             my %loop = (
204             'start' => $third,
205             'end' => $twothirds,
206             'fraction' => 0,
207             'type' => 0,
208             );
209             $write -> add_sampler_loop( %loop );
210              
211             =cut
212              
213             sub add_sampler_loop {
214 2     2 1 50 my ($self, @args) = @_;
215 2         11 return $self -> {header} -> add_sampler_loop( @args );
216             }
217              
218             =head2 add_display
219              
220             =cut
221              
222             sub add_display {
223 2     2 1 13 my ($self, @args) = @_;
224 2         10 return $self -> {header} -> add_display( @args );
225             }
226              
227             =head2 set_info
228              
229             Sets information to be contained in the wav file.
230              
231             $write -> set_info( 'artist' => 'Nightmares on Wax', 'name' => 'Mission Venice' );
232              
233             =cut
234              
235             sub set_info {
236 1     1 1 16 my ($self, %info) = @_;
237 1         1 $self -> {details} -> {info} = { %{ $self -> {details} -> {info} }, %info };
  1         11  
238             }
239              
240             =head2 file_name
241              
242             Returns the current filename.
243              
244             my $file = $write -> file_name();
245              
246             =cut
247              
248             sub file_name {
249 0     0 1 0 my $self = shift;
250 0         0 return $self -> {out_file};
251             }
252              
253             =head2 write
254              
255             Adds a sample to the current file.
256              
257             $write -> write( @sample_channels );
258              
259             Each element in @sample_channels should be in the range of;
260              
261             where $samp_max = ( 2 ** bits_per_sample ) / 2
262             -$samp_max to +$samp_max
263              
264             =cut
265              
266             sub write {
267 22050     22050 1 229719 my ($self, @args) = @_;
268 22050         51194 my $channels = $self -> {details} -> {channels};
269 22050 50       43675 if ( $self -> {use_offset} ) {
270 22050         41952 return $self -> write_raw( pack 'C'.$channels, map { $_ + $self -> {use_offset} } @args );
  22050         89110  
271             } else {
272             #TODO: performance: when we move to _init_write_sub, just use:
273             #32: pack 'V1', ...
274             #24: substr pack('V1', ...), 3
275             #16: pack 'v1', ...
276 0         0 my $bytes_per_sample = $self->{details}->{bits_sample} >> 3;
277 1     1   7 use bytes;
  1         2  
  1         10  
278 0         0 my @samples = map { substr pack('V1', $_), 0, $bytes_per_sample } @args;
  0         0  
279             #warn "bits/sample: $self->{details}->{bits_sample}, bytes/sample: $bytes_per_sample";
280             #warn "output samples(".scalar @samples."): ".join "-", map ord, split //, join '', @samples;
281 0         0 return $self -> write_raw( join '', @samples );
282             }
283             }
284              
285             =head2 write_raw
286              
287             Adds some pre-packed data to the current file.
288              
289             $write -> write_raw( $data, $data_length );
290              
291             Where;
292              
293             $data is the packed data
294             $data_length (optional) is the length in bytes of the data
295              
296             =cut
297              
298             sub write_raw {
299 22096     22096 1 28885 my $self = shift;
300 22096         26464 my $data = shift;
301 22096         25157 my $len = shift;
302 22096 100       50474 $len = length $data unless $len;
303 22096 50       48281 return unless $len;
304 22096         31594 my $wrote = $len;
305 22096 50       42714 if ( $self -> {use_cache} ) {
306 22096         28185 $self -> {write_cache} .= $data;
307 22096         27829 my $cache_len = length $self -> {write_cache};
308 22096 100       58655 $self -> _purge_cache( $cache_len ) unless $cache_len < $self -> {cache_size};
309             } else {
310 0         0 $wrote = syswrite $self -> {handle}, $data, $len;
311             }
312              
313 22096         27754 $self -> {pos} += $wrote;
314 22096         88102 return $wrote;
315             }
316              
317             =head2 write_raw_samples
318              
319             Adds some pre-packed data to the current file, returns number of samples written.
320              
321             $write -> write_raw_samples( $data, $data_length );
322              
323             Where;
324              
325             $data is the packed data
326             $data_length (optional) is the length in bytes of the data
327              
328             =cut
329              
330             sub write_raw_samples {
331 0     0 1 0 my ($self, @args) = @_;
332 0         0 my $written = $self -> write_raw( @args );
333 0         0 return $written / $self -> {details} -> {block_align};
334             }
335              
336             ####################
337              
338             sub _start_file {
339 2     2   3 my $self = shift;
340 2         4 my( $file, $details, $tools, $handle ) = map { $self -> {$_} } qw( out_file details tools handle );
  8         19  
341 2         19 my $header = Audio::Wav::Write::Header -> new( $file, $details, $tools, $handle );
342 2         5 $self -> {header} = $header;
343 2         120 my $data = $header -> start();
344 2         9 $self -> write_raw( $data );
345 2         7 $self -> {pos} = 0;
346             }
347              
348             sub _purge_cache {
349 12     12   20 my $self = shift;
350 12         24 my $len = shift;
351 12 50       32 return unless $self -> {write_cache};
352 12         63 my $cache = $self -> {write_cache};
353 12 100       32 $len = length $cache unless $len;
354 12         751 my $res = syswrite $self -> {handle}, $cache, $len;
355 12         36 $self -> {write_cache} = undef;
356             }
357              
358             sub _init {
359 2     2   4 my $self = shift;
360 2         8 my $details = $self -> {details};
361 2         4 my $output = {};
362 2         3 my @missing;
363 2         6 my @needed = qw ( bits_sample channels sample_rate );
364 2         5 my @wanted = qw ( block_align bytes_sec info wave-ex );
365              
366 2         6 foreach my $need ( @needed ) {
367 6 50 33     30 if ( exists( $details -> {$need} ) && $details -> {$need} ) {
368 6         19 $output -> {$need} = $details -> {$need};
369             } else {
370 0         0 push @missing, $need;
371             }
372             }
373 2 50       7 return $self -> _error('I need the following parameters supplied: ' . join ', ', @missing ) if @missing;
374 2         4 foreach my $want ( @wanted ) {
375 8 100 100     45 next unless ( exists( $details -> {$want} ) && $details -> {$want} );
376 3         7 $output -> {$want} = $details -> {$want};
377             }
378 2 100       8 unless ( exists $details -> {block_align} ) {
379 1         3 my( $channels, $bits ) = map { $output -> {$_} } qw( channels bits_sample );
  2         15  
380 1 50       5 my $mod_bits = $bits % 8 ? 1 : 0;
381 1         620 $mod_bits += int $bits / 8;
382 1         5 $output -> {block_align} = $channels * $mod_bits;
383             }
384 2 100       7 unless ( exists $output -> {bytes_sec} ) {
385 1         3 my( $rate, $block ) = map { $output -> {$_} } qw( sample_rate block_align );
  2         6  
386 1         4 $output -> {bytes_sec} = $rate * $block;
387             }
388 2 100       7 unless ( exists $output -> {info} ) {
389 1         3 $output -> {info} = {};
390             }
391              
392 2         7 $self -> {details} = $output;
393             }
394              
395             sub _examine_details {
396 2     2   3 my $self = shift;
397 2         4 my $details = shift;
398 6 100       20 my( $cue, $label, $note ) =
399 2         3 map { exists( $details -> {$_} ) ? $details -> {$_} : {} }
400             qw( cue labl note );
401 2         39 my $block_align = $self -> {details} -> {block_align};
402 2         4 my $tools = $self -> {tools};
403 2         4 foreach my $id ( sort keys %{$cue} ) { # <-- Thanks to jeremyd713@hotmail.com
  2         16  
404 3         7 my $pos = $cue -> {$id} -> {position};
405 3 50       11 $pos *= $block_align if $tools -> is_01compatible();
406 6 50       20 my( $in_label, $in_note ) =
407 3         5 map { exists( $_ -> {$id} ) ? $_ -> {$id} : '' }
408             ( $label, $note );
409 3         11 $self -> add_cue( $pos, $in_label, $in_note );
410             }
411 2 100       8 if ( exists $details -> {sampler} ) {
412 1         2 my $sampler = $details -> {sampler};
413 1         3 my $loops = delete $sampler -> {loop};
414 1         3 $self -> set_sampler_info( %{$sampler} );
  1         6  
415 1         2 foreach my $loop ( @{$loops} ) {
  1         3  
416 1         4 $self -> add_sampler_loop( %{$loop} );
  1         7  
417             }
418             }
419 2 100       13 if ( exists $details -> {display} ) {
420 1         1 my @display = @{ $details -> {display} };
  1         3  
421 1         2 my @fields = qw( id data );
422 1         2 $self -> add_display( map { $fields[$_] => $display[$_] } 0, 1 );
  2         7  
423             }
424             }
425              
426             sub _error {
427 0     0     my ($self, @args) = @_;
428 0           return $self -> {tools} -> error( $self -> {out_file}, @args );
429             }
430              
431             =head1 AUTHORS
432              
433             Nick Peskett (see http://www.peskett.co.uk/ for contact details).
434             Kurt George Gjerde . (0.02-0.03)
435              
436             =cut
437              
438             1;