nu_wav 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/VERSION +1 -1
- data/lib/nu_wav.rb +93 -23
- data/nu_wav.gemspec +2 -2
- data/test/test_nu_wav.rb +16 -0
- metadata +4 -4
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.3.
|
|
1
|
+
0.3.1
|
data/lib/nu_wav.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# http://www.prss.org/contentdepot/automation_specifications.cfm
|
|
2
2
|
# Bill Kelly <billk <at> cts.com> http://article.gmane.org/gmane.comp.lang.ruby.general/43110
|
|
3
|
-
|
|
4
3
|
# BWF intro http://en.wikipedia.org/wiki/Broadcast_Wave_Format
|
|
5
4
|
# BWF basics http://tech.ebu.ch/docs/tech/tech3285.pdf
|
|
6
5
|
# BWF mpeg http://tech.ebu.ch/docs/tech/tech3285s1.pdf
|
|
@@ -8,12 +7,17 @@
|
|
|
8
7
|
require 'rubygems'
|
|
9
8
|
require 'mp3info'
|
|
10
9
|
require 'date'
|
|
10
|
+
require 'tempfile'
|
|
11
11
|
|
|
12
12
|
module NuWav
|
|
13
13
|
|
|
14
14
|
DEBUG = false
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# 1 is standard integer based, 3 is the floating point PCM
|
|
17
|
+
PCM_INTEGER_COMPRESSION = 1
|
|
18
|
+
PCM_FLOATING_COMPRESSION = 3
|
|
19
|
+
PCM_COMPRESSION = [PCM_INTEGER_COMPRESSION, PCM_FLOATING_COMPRESSION]
|
|
20
|
+
|
|
17
21
|
MPEG_COMPRESSION = 80
|
|
18
22
|
|
|
19
23
|
ACM_MPEG_LAYER1 = 1
|
|
@@ -51,12 +55,14 @@ module NuWav
|
|
|
51
55
|
def parse(wave_file)
|
|
52
56
|
NuWav::WaveFile.log "Processing wave file #{wave_file.inspect}...."
|
|
53
57
|
File.open(wave_file, File::RDWR) do |f|
|
|
58
|
+
|
|
54
59
|
#only for windows, make sure we are operating in binary mode
|
|
55
60
|
f.binmode
|
|
56
61
|
#start at the very beginning, a very good place to start
|
|
57
62
|
f.seek(0)
|
|
58
63
|
|
|
59
64
|
riff, riff_length = read_chunk_header(f)
|
|
65
|
+
NuWav::WaveFile.log "riff: #{riff}"
|
|
60
66
|
NuWav::WaveFile.log "riff_length: #{riff_length}"
|
|
61
67
|
raise NotRIFFFormat unless riff == 'RIFF'
|
|
62
68
|
riff_end = f.tell + riff_length
|
|
@@ -66,23 +72,35 @@ module NuWav
|
|
|
66
72
|
|
|
67
73
|
@header = RiffChunk.new(riff, riff_length, riff_type)
|
|
68
74
|
|
|
69
|
-
while f.tell
|
|
75
|
+
while (f.tell + 8) <= riff_end
|
|
76
|
+
NuWav::WaveFile.log "while #{f.tell} < #{riff_end}"
|
|
70
77
|
chunk_name, chunk_length = read_chunk_header(f)
|
|
71
78
|
fpos = f.tell
|
|
72
79
|
|
|
73
80
|
NuWav::WaveFile.log "found chunk: '#{chunk_name}', size #{chunk_length}"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
|
|
82
|
+
if chunk_name && chunk_length
|
|
83
|
+
|
|
84
|
+
self.chunks[chunk_name.to_sym] = chunk_class(chunk_name).parse(chunk_name, chunk_length, f)
|
|
85
|
+
|
|
86
|
+
NuWav::WaveFile.log "about to do a seek..."
|
|
87
|
+
NuWav::WaveFile.log "f.seek #{fpos} + #{self.chunks[chunk_name.to_sym].size}"
|
|
88
|
+
f.seek(fpos + self.chunks[chunk_name.to_sym].size)
|
|
89
|
+
NuWav::WaveFile.log "seek done"
|
|
90
|
+
else
|
|
91
|
+
NuWav::WaveFile.log "chunk or length was off - remainder of file does not parse properly: #{riff_end} - #{fpos} = #{riff_end - fpos}"
|
|
92
|
+
f.seek(riff_end)
|
|
93
|
+
end
|
|
77
94
|
end
|
|
78
95
|
end
|
|
79
|
-
@chunks.each{|k,v| NuWav::WaveFile.log "#{k}: #{v}\n\n" unless k ==
|
|
96
|
+
@chunks.each{|k,v| NuWav::WaveFile.log "#{k}: #{v}\n\n" unless k.to_s == 'data'}
|
|
97
|
+
NuWav::WaveFile.log "parse done"
|
|
80
98
|
end
|
|
81
99
|
|
|
82
100
|
def duration
|
|
83
101
|
fmt = @chunks[:fmt]
|
|
84
102
|
|
|
85
|
-
if (fmt.compression_code.to_i
|
|
103
|
+
if (PCM_COMPRESSION.include?(fmt.compression_code.to_i))
|
|
86
104
|
data = @chunks[:data]
|
|
87
105
|
data.size / (fmt.sample_rate * fmt.number_of_channels * (fmt.sample_bits / 8))
|
|
88
106
|
elsif (fmt.compression_code.to_i == MPEG_COMPRESSION)
|
|
@@ -93,6 +111,14 @@ module NuWav
|
|
|
93
111
|
raise "Duration implemented for PCM and MEPG files only."
|
|
94
112
|
end
|
|
95
113
|
end
|
|
114
|
+
|
|
115
|
+
def is_mpeg?
|
|
116
|
+
(@chunks[:fmt] && (@chunks[:fmt].compression_code.to_i == MPEG_COMPRESSION))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def is_pcm?
|
|
120
|
+
(@chunks[:fmt] && (PCM_COMPRESSION.include?(@chunks[:fmt].compression_code.to_i)))
|
|
121
|
+
end
|
|
96
122
|
|
|
97
123
|
def to_s
|
|
98
124
|
out = "NuWav:#{@header}\n"
|
|
@@ -109,13 +135,17 @@ module NuWav
|
|
|
109
135
|
NuWav::WaveFile.log "NuWav::WaveFile.to_file: file_name = #{file_name}"
|
|
110
136
|
|
|
111
137
|
#get all the chunks together to get final length
|
|
112
|
-
chunks_out = [:fmt, :fact, :mext, :bext, :cart, :data].inject([]) do |list, chunk|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
138
|
+
chunks_out = [:fmt, :fact, :mext, :bext, :cart, :data].inject([]) do |list, chunk|
|
|
139
|
+
if self.chunks[chunk]
|
|
140
|
+
out = self.chunks[chunk].to_binary
|
|
141
|
+
NuWav::WaveFile.log out.length
|
|
142
|
+
list << out
|
|
143
|
+
end
|
|
116
144
|
list
|
|
117
145
|
end
|
|
118
146
|
|
|
147
|
+
# TODO: handle other chunks not in the above list, but that might have been in a parsed wav
|
|
148
|
+
|
|
119
149
|
riff_length = chunks_out.inject(0){|sum, chunk| sum += chunk.size}
|
|
120
150
|
NuWav::WaveFile.log "NuWav::WaveFile.to_file: riff_length = #{riff_length}"
|
|
121
151
|
|
|
@@ -182,12 +212,8 @@ module NuWav
|
|
|
182
212
|
|
|
183
213
|
#mext chunk
|
|
184
214
|
mext = MextChunk.new
|
|
185
|
-
|
|
186
|
-
mext.sound_information
|
|
187
|
-
|
|
188
|
-
# from what I can tell, a 7 indicates a homogenous 44100 or 22050 mpeg audio with no padding
|
|
189
|
-
# if there is padding, it is more complicated, see sectin 2.2 here: http://tech.ebu.ch/docs/tech/tech3285s1.pdf
|
|
190
|
-
mext.sound_information = 5 if mp3info.header[:padding]
|
|
215
|
+
mext.sound_information = 5
|
|
216
|
+
mext.sound_information += 2 if mp3info.header[:padding]
|
|
191
217
|
mext.frame_size = calculate_mpeg_frame_size(mp3info)
|
|
192
218
|
mext.ancillary_data_length = 0
|
|
193
219
|
mext.ancillary_data_def = 0
|
|
@@ -251,7 +277,12 @@ module NuWav
|
|
|
251
277
|
end
|
|
252
278
|
|
|
253
279
|
def chunk_class(name)
|
|
254
|
-
|
|
280
|
+
begin
|
|
281
|
+
constantize("NuWav::#{camelize("#{name}_chunk")}")
|
|
282
|
+
rescue NameError
|
|
283
|
+
NuWav::Chunk
|
|
284
|
+
end
|
|
285
|
+
|
|
255
286
|
end
|
|
256
287
|
|
|
257
288
|
# File vendor/rails/activesupport/lib/active_support/inflector.rb, line 147
|
|
@@ -273,7 +304,7 @@ module NuWav
|
|
|
273
304
|
|
|
274
305
|
def self.log(m)
|
|
275
306
|
if NuWav::DEBUG
|
|
276
|
-
|
|
307
|
+
NuWav::WaveFile.log "#{Time.now}: NuWav: #{m}"
|
|
277
308
|
end
|
|
278
309
|
end
|
|
279
310
|
|
|
@@ -309,10 +340,12 @@ module NuWav
|
|
|
309
340
|
end
|
|
310
341
|
|
|
311
342
|
def write_dword(val)
|
|
343
|
+
val ||= 0
|
|
312
344
|
[val].pack('V')
|
|
313
345
|
end
|
|
314
346
|
|
|
315
347
|
def write_word(val)
|
|
348
|
+
val ||= 0
|
|
316
349
|
[val].pack('v')
|
|
317
350
|
end
|
|
318
351
|
|
|
@@ -547,16 +580,53 @@ module NuWav
|
|
|
547
580
|
end
|
|
548
581
|
|
|
549
582
|
class DataChunk < Chunk
|
|
550
|
-
|
|
551
|
-
|
|
583
|
+
attr_accessor :tmp_data_file
|
|
584
|
+
|
|
585
|
+
def self.parse(id, size, file)
|
|
552
586
|
|
|
587
|
+
# tmp_data = File.open('./data_chunk.mp2', 'wb')
|
|
588
|
+
tmp_data = Tempfile.open('data_chunk')
|
|
589
|
+
tmp_data.binmode
|
|
590
|
+
|
|
591
|
+
remaining = size
|
|
592
|
+
while (remaining > 0 && !file.eof?)
|
|
593
|
+
read_bytes = [128, remaining].min
|
|
594
|
+
tmp_data << file.read(read_bytes)
|
|
595
|
+
remaining -= read_bytes
|
|
596
|
+
end
|
|
597
|
+
tmp_data.rewind
|
|
598
|
+
chunk = self.new(id, size, tmp_data)
|
|
599
|
+
|
|
600
|
+
return chunk
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def initialize(id=nil, size=nil, tmp_data_file=nil)
|
|
604
|
+
@id, @size, @tmp_data_file = id, size, tmp_data_file
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
def data
|
|
608
|
+
f = ''
|
|
609
|
+
if self.tmp_data_file
|
|
610
|
+
NuWav::WaveFile.log "we have a tmp_data_file!"
|
|
611
|
+
self.tmp_data_file.rewind
|
|
612
|
+
f = self.tmp_data_file.read
|
|
613
|
+
self.tmp_data_file.rewind
|
|
614
|
+
else
|
|
615
|
+
NuWav::WaveFile.log "we have NO tmp_data_file!"
|
|
616
|
+
end
|
|
617
|
+
f
|
|
618
|
+
end
|
|
553
619
|
|
|
554
620
|
def to_s
|
|
555
621
|
"<chunk type:data (size:#{data.size})/>"
|
|
556
622
|
end
|
|
557
623
|
|
|
558
624
|
def to_binary
|
|
559
|
-
|
|
625
|
+
NuWav::WaveFile.log "data chunk to_binary"
|
|
626
|
+
d = self.data
|
|
627
|
+
NuWav::WaveFile.log "got data size = #{d.size} #{d[0,10]}"
|
|
628
|
+
out = "data" + write_dword(d.size) + d
|
|
629
|
+
out
|
|
560
630
|
end
|
|
561
631
|
|
|
562
632
|
end
|
data/nu_wav.gemspec
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
Gem::Specification.new do |s|
|
|
7
7
|
s.name = %q{nu_wav}
|
|
8
|
-
s.version = "0.3.
|
|
8
|
+
s.version = "0.3.1"
|
|
9
9
|
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
|
11
11
|
s.authors = ["kookster"]
|
|
12
|
-
s.date = %q{2011-
|
|
12
|
+
s.date = %q{2011-05-28}
|
|
13
13
|
s.description = %q{NuWav is a pure ruby audio WAV file parser and writer. It supports Broadcast Wave Format (BWF), inclluding MPEG audio data, and the public radio standard cart chunk.}
|
|
14
14
|
s.email = %q{andrew@beginsinwonder.com}
|
|
15
15
|
s.extra_rdoc_files = [
|
data/test/test_nu_wav.rb
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
require 'helper'
|
|
2
2
|
require 'nu_wav'
|
|
3
|
+
require 'tempfile'
|
|
3
4
|
|
|
4
5
|
class TestNuWav < Test::Unit::TestCase
|
|
5
6
|
include NuWav
|
|
6
7
|
|
|
7
8
|
def test_parse_wav
|
|
9
|
+
memory_usage = `ps -o rss= -p #{Process.pid}`.to_i # in kilobytes
|
|
10
|
+
puts "begin test: #{memory_usage/1024} mb"
|
|
8
11
|
w = WaveFile.parse(File.expand_path(File.dirname(__FILE__) + '/files/test_basic.wav'))
|
|
12
|
+
puts w
|
|
9
13
|
assert_equal 4260240, w.header.size
|
|
10
14
|
|
|
11
15
|
assert_equal 2, w.chunks.size
|
|
@@ -20,10 +24,20 @@ class TestNuWav < Test::Unit::TestCase
|
|
|
20
24
|
|
|
21
25
|
data = w.chunks[:data]
|
|
22
26
|
assert_equal 4260204, data.size
|
|
27
|
+
|
|
28
|
+
w.to_file('test_out.wav')
|
|
29
|
+
memory_usage = `ps -o rss= -p #{Process.pid}`.to_i # in kilobytes
|
|
30
|
+
puts "end of test: #{memory_usage/1024} mb"
|
|
23
31
|
end
|
|
24
32
|
|
|
25
33
|
def test_parse_wav_with_bwf_and_cart_chunk
|
|
34
|
+
memory_usage = `ps -o rss= -p #{Process.pid}`.to_i # in kilobytes
|
|
35
|
+
puts "begin test: #{memory_usage/1024} mb"
|
|
36
|
+
|
|
26
37
|
w = WaveFile.parse(File.expand_path(File.dirname(__FILE__) + '/files/AfropopW_040_SGMT01.wav'))
|
|
38
|
+
memory_usage = `ps -o rss= -p #{Process.pid}`.to_i # in kilobytes
|
|
39
|
+
puts "after parse: #{memory_usage/1024} mb"
|
|
40
|
+
|
|
27
41
|
assert_equal 6, w.chunks.size
|
|
28
42
|
|
|
29
43
|
# duration is calculated differently for mpeg and pcm
|
|
@@ -67,6 +81,8 @@ class TestNuWav < Test::Unit::TestCase
|
|
|
67
81
|
|
|
68
82
|
# data
|
|
69
83
|
assert_equal 57040521, w.chunks[:data].size
|
|
84
|
+
memory_usage = `ps -o rss= -p #{Process.pid}`.to_i # in kilobytes
|
|
85
|
+
puts "end of test: #{memory_usage/1024} mb"
|
|
70
86
|
end
|
|
71
87
|
|
|
72
88
|
def unpad(str)
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nu_wav
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
hash:
|
|
4
|
+
hash: 17
|
|
5
5
|
prerelease:
|
|
6
6
|
segments:
|
|
7
7
|
- 0
|
|
8
8
|
- 3
|
|
9
|
-
-
|
|
10
|
-
version: 0.3.
|
|
9
|
+
- 1
|
|
10
|
+
version: 0.3.1
|
|
11
11
|
platform: ruby
|
|
12
12
|
authors:
|
|
13
13
|
- kookster
|
|
@@ -15,7 +15,7 @@ autorequire:
|
|
|
15
15
|
bindir: bin
|
|
16
16
|
cert_chain: []
|
|
17
17
|
|
|
18
|
-
date: 2011-
|
|
18
|
+
date: 2011-05-28 00:00:00 -04:00
|
|
19
19
|
default_executable:
|
|
20
20
|
dependencies:
|
|
21
21
|
- !ruby/object:Gem::Dependency
|