nu_wav 0.3.4 → 0.4.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE +2 -0
- data/Rakefile +2 -46
- data/lib/nu_wav.rb +5 -608
- data/lib/nu_wav/chunk.rb +331 -0
- data/lib/nu_wav/version.rb +3 -0
- data/lib/nu_wav/wave_file.rb +286 -0
- data/nu_wav.gemspec +20 -45
- data/test/files/test.mp2 +0 -0
- data/test/files/test_basic.wav +0 -0
- data/test/files/test_bwf.wav +0 -0
- data/test/test_nu_wav.rb +40 -25
- metadata +81 -64
- data/VERSION +0 -1
data/lib/nu_wav/chunk.rb
ADDED
@@ -0,0 +1,331 @@
|
|
1
|
+
module NuWav
|
2
|
+
class Chunk
|
3
|
+
attr_accessor :id, :size, :raw
|
4
|
+
|
5
|
+
def self.parse(id, size, file)
|
6
|
+
raw = file.read(size)
|
7
|
+
chunk = self.new(id, size, raw)
|
8
|
+
chunk.parse
|
9
|
+
return chunk
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(id=nil, size=nil, raw=nil)
|
13
|
+
@id, @size, @raw = id, size, raw
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse
|
17
|
+
end
|
18
|
+
|
19
|
+
def read_dword(start)
|
20
|
+
@raw[start..(start+3)].unpack('V').first
|
21
|
+
end
|
22
|
+
|
23
|
+
def read_word(start)
|
24
|
+
@raw[start..(start+1)].unpack('v').first
|
25
|
+
end
|
26
|
+
|
27
|
+
def read_char(start, length=(@raw.length-start))
|
28
|
+
(@raw[start..(start+length-1)] || '').strip
|
29
|
+
end
|
30
|
+
|
31
|
+
def write_dword(val)
|
32
|
+
val ||= 0
|
33
|
+
[val].pack('V')
|
34
|
+
end
|
35
|
+
|
36
|
+
def write_word(val)
|
37
|
+
val ||= 0
|
38
|
+
[val].pack('v')
|
39
|
+
end
|
40
|
+
|
41
|
+
def write_char(val, length=nil)
|
42
|
+
val ||= ''
|
43
|
+
val = val.to_s
|
44
|
+
length ||= val.length
|
45
|
+
# NuWav::WaveFile.log "length:#{length} val.length:#{val.length} val:#{val}"
|
46
|
+
padding = "\0" * [(length - val.length), 0].max
|
47
|
+
out = val[0,length] + padding
|
48
|
+
# NuWav::WaveFile.log out
|
49
|
+
out
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_binary
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
class RiffChunk
|
58
|
+
attr_accessor :id, :size, :riff_type
|
59
|
+
|
60
|
+
def initialize(riff_name, riff_length, riff_type)
|
61
|
+
@id, @size, @riff_type = riff_name, riff_length, riff_type
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_s
|
65
|
+
"<chunk type:riff id:#{@id} size:#{@size} type:#{@riff_type} />"
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
class FmtChunk < Chunk
|
71
|
+
|
72
|
+
attr_accessor :compression_code, :number_of_channels, :sample_rate, :byte_rate, :block_align, :sample_bits, :extra_size, :extra,
|
73
|
+
:head_layer, :head_bit_rate, :head_mode, :head_mode_ext, :head_emphasis, :head_flags, :pts_low, :pts_high
|
74
|
+
|
75
|
+
def parse
|
76
|
+
NuWav::WaveFile.log "@raw.size = #{@raw.size}"
|
77
|
+
@compression_code = read_word(0)
|
78
|
+
@number_of_channels = read_word(2)
|
79
|
+
@sample_rate = read_dword(4)
|
80
|
+
@byte_rate = read_dword(8)
|
81
|
+
@block_align = read_word(12)
|
82
|
+
@sample_bits = read_word(14)
|
83
|
+
@extra_size = read_word(16)
|
84
|
+
|
85
|
+
if (@compression_code.to_i == MPEG_COMPRESSION)
|
86
|
+
@head_layer = read_word(18)
|
87
|
+
@head_bit_rate = read_dword(20)
|
88
|
+
@head_mode = read_word(24)
|
89
|
+
@head_mode_ext = read_word(26)
|
90
|
+
@head_emphasis = read_word(28)
|
91
|
+
@head_flags = read_word(30)
|
92
|
+
@pts_low = read_dword(32)
|
93
|
+
@pts_high = read_dword(36)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def to_binary
|
98
|
+
out = ''
|
99
|
+
out += write_word(@compression_code)
|
100
|
+
out += write_word(@number_of_channels)
|
101
|
+
out += write_dword(@sample_rate)
|
102
|
+
out += write_dword(@byte_rate)
|
103
|
+
out += write_word(@block_align)
|
104
|
+
out += write_word(@sample_bits)
|
105
|
+
out += write_word(@extra_size)
|
106
|
+
|
107
|
+
if (@compression_code.to_i == MPEG_COMPRESSION)
|
108
|
+
out += write_word(@head_layer)
|
109
|
+
out += write_dword(@head_bit_rate)
|
110
|
+
out += write_word(@head_mode)
|
111
|
+
out += write_word(@head_mode_ext)
|
112
|
+
out += write_word(@head_emphasis)
|
113
|
+
out += write_word(@head_flags)
|
114
|
+
out += write_dword(@pts_low)
|
115
|
+
out += write_dword(@pts_high)
|
116
|
+
end
|
117
|
+
"fmt " + write_dword(out.size) + out
|
118
|
+
end
|
119
|
+
|
120
|
+
def to_s
|
121
|
+
extra = if (@compression_code.to_i == MPEG_COMPRESSION)
|
122
|
+
", head_layer:#{head_layer}, head_bit_rate:#{head_bit_rate}, head_mode:#{head_mode}, head_mode_ext:#{head_mode_ext}, head_emphasis:#{head_emphasis}, head_flags:#{head_flags}, pts_low:#{pts_low}, pts_high:#{pts_high}"
|
123
|
+
else
|
124
|
+
""
|
125
|
+
end
|
126
|
+
"<chunk type:fmt compression_code:#{compression_code}, number_of_channels:#{number_of_channels}, sample_rate:#{sample_rate}, byte_rate:#{byte_rate}, block_align:#{block_align}, sample_bits:#{sample_bits}, extra_size:#{extra_size} #{extra} />"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
class FactChunk < Chunk
|
131
|
+
attr_accessor :samples_number
|
132
|
+
|
133
|
+
def parse
|
134
|
+
@samples_number = read_dword(0)
|
135
|
+
end
|
136
|
+
|
137
|
+
def to_s
|
138
|
+
"<chunk type:fact samples_number:#{@samples_number} />"
|
139
|
+
end
|
140
|
+
|
141
|
+
def to_binary
|
142
|
+
"fact" + write_dword(4) + write_dword(@samples_number)
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
class MextChunk < Chunk
|
148
|
+
attr_accessor :sound_information, :frame_size, :ancillary_data_length, :ancillary_data_def, :reserved
|
149
|
+
|
150
|
+
def parse
|
151
|
+
@sound_information = read_word(0)
|
152
|
+
@frame_size = read_word(2)
|
153
|
+
@ancillary_data_length = read_word(4)
|
154
|
+
@ancillary_data_def = read_word(6)
|
155
|
+
@reserved = read_char(8,4)
|
156
|
+
end
|
157
|
+
|
158
|
+
def to_s
|
159
|
+
"<chunk type:mext sound_information:(#{sound_information}) #{(0..15).inject(''){|s,x| "#{s}#{sound_information[x]}"}}, frame_size:#{frame_size}, ancillary_data_length:#{ancillary_data_length}, ancillary_data_def:#{(0..15).inject(''){|s,x| "#{s}#{ancillary_data_def[x]}"}}, reserved:'#{reserved}' />"
|
160
|
+
end
|
161
|
+
|
162
|
+
def to_binary
|
163
|
+
out = "mext" + write_dword(12)
|
164
|
+
out += write_word(@sound_information)
|
165
|
+
out += write_word(@frame_size)
|
166
|
+
out += write_word(@ancillary_data_length)
|
167
|
+
out += write_word(@ancillary_data_def)
|
168
|
+
out += write_char(@reserved, 4)
|
169
|
+
out
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
class BextChunk < Chunk
|
174
|
+
attr_accessor :description, :originator, :originator_reference, :origination_date, :origination_time, :time_reference_low, :time_reference_high,
|
175
|
+
:version, :umid, :reserved, :coding_history
|
176
|
+
|
177
|
+
def parse
|
178
|
+
@description = read_char(0,256)
|
179
|
+
@originator = read_char(256,32)
|
180
|
+
@originator_reference = read_char(288,32)
|
181
|
+
@origination_date = read_char(320,10)
|
182
|
+
@origination_time = read_char(330,8)
|
183
|
+
@time_reference_low = read_dword(338)
|
184
|
+
@time_reference_high = read_dword(342)
|
185
|
+
@version = read_word(346)
|
186
|
+
@umid = read_char(348,64)
|
187
|
+
@reserved = read_char(412,190)
|
188
|
+
@coding_history = read_char(602)
|
189
|
+
end
|
190
|
+
|
191
|
+
def to_s
|
192
|
+
"<chunk type:bext description:'#{description}', originator:'#{originator}', originator_reference:'#{originator_reference}', origination_date:'#{origination_date}', origination_time:'#{origination_time}', time_reference_low:#{time_reference_low}, time_reference_high:#{time_reference_high}, version:#{version}, umid:#{umid}, reserved:'#{reserved}', coding_history:#{coding_history} />"
|
193
|
+
end
|
194
|
+
|
195
|
+
def to_binary
|
196
|
+
out = "bext" + write_dword(602 + @coding_history.length )
|
197
|
+
out += write_char(@description, 256)
|
198
|
+
out += write_char(@originator, 32)
|
199
|
+
out += write_char(@originator_reference, 32)
|
200
|
+
out += write_char(@origination_date, 10)
|
201
|
+
out += write_char(@origination_time, 8)
|
202
|
+
out += write_dword(@time_reference_low)
|
203
|
+
out += write_dword(@time_reference_high)
|
204
|
+
out += write_word(@version)
|
205
|
+
out += write_char(@umid, 64)
|
206
|
+
out += write_char(@reserved, 190)
|
207
|
+
out += write_char(@coding_history)
|
208
|
+
# make sure coding history ends in '\r\n'
|
209
|
+
out
|
210
|
+
end
|
211
|
+
|
212
|
+
end
|
213
|
+
|
214
|
+
class CartChunk < Chunk
|
215
|
+
attr_accessor :version, :title, :artist, :cut_id, :client_id, :category, :classification, :out_cue, :start_date, :start_time, :end_date, :end_time,
|
216
|
+
:producer_app_id, :producer_app_version, :user_def, :level_reference, :post_timer, :reserved, :url, :tag_text
|
217
|
+
|
218
|
+
def parse
|
219
|
+
@version = read_char(0,4)
|
220
|
+
@title = read_char(4,64)
|
221
|
+
@artist = read_char(68,64)
|
222
|
+
@cut_id = read_char(132,64)
|
223
|
+
@client_id = read_char(196,64)
|
224
|
+
@category = read_char(260,64)
|
225
|
+
@classification = read_char(324,64)
|
226
|
+
@out_cue = read_char(388,64)
|
227
|
+
@start_date = read_char(452,10)
|
228
|
+
@start_time = read_char(462,8)
|
229
|
+
@end_date = read_char(470,10)
|
230
|
+
@end_time = read_char(480,8)
|
231
|
+
@producer_app_id = read_char(488,64)
|
232
|
+
@producer_app_version = read_char(552,64)
|
233
|
+
@user_def = read_char(616,64)
|
234
|
+
@level_reference = read_dword(680)
|
235
|
+
@post_timer = read_char(684,64)
|
236
|
+
@reserved = read_char(748,276)
|
237
|
+
@url = read_char(1024,1024)
|
238
|
+
@tag_text = read_char(2048)
|
239
|
+
end
|
240
|
+
|
241
|
+
def to_s
|
242
|
+
"<chunk type:cart version:#{version}, title:#{title}, artist:#{artist}, cut_id:#{cut_id}, client_id:#{client_id}, category:#{category}, classification:#{classification}, out_cue:#{out_cue}, start_date:#{start_date}, start_time:#{start_time}, end_date:#{end_date}, end_time:#{end_time}, producer_app_id:#{producer_app_id}, producer_app_version:#{producer_app_version}, user_def:#{user_def}, level_reference:#{level_reference}, post_timer:#{post_timer}, reserved:#{reserved}, url:#{url}, tag_text:#{tag_text} />"
|
243
|
+
end
|
244
|
+
|
245
|
+
def to_binary
|
246
|
+
out = "cart" + write_dword(2048 + @tag_text.length )
|
247
|
+
out += write_char(@version,4)
|
248
|
+
out += write_char(@title,64)
|
249
|
+
out += write_char(@artist,64)
|
250
|
+
out += write_char(@cut_id,64)
|
251
|
+
out += write_char(@client_id,64)
|
252
|
+
out += write_char(@category,64)
|
253
|
+
out += write_char(@classification,64)
|
254
|
+
out += write_char(@out_cue,64)
|
255
|
+
out += write_char(@start_date,10)
|
256
|
+
out += write_char(@start_time,8)
|
257
|
+
out += write_char(@end_date,10)
|
258
|
+
out += write_char(@end_time,8)
|
259
|
+
out += write_char(@producer_app_id,64)
|
260
|
+
out += write_char(@producer_app_version,64)
|
261
|
+
out += write_char(@user_def,64)
|
262
|
+
out += write_dword(@level_reference)
|
263
|
+
out += write_char(@post_timer,64)
|
264
|
+
out += write_char(@reserved,276)
|
265
|
+
out += write_char(@url,1024)
|
266
|
+
out += write_char(@tag_text)
|
267
|
+
out
|
268
|
+
end
|
269
|
+
|
270
|
+
end
|
271
|
+
|
272
|
+
class DataChunk < Chunk
|
273
|
+
attr_accessor :tmp_data_file
|
274
|
+
|
275
|
+
def self.parse(id, size, file)
|
276
|
+
|
277
|
+
# tmp_data = File.open('./data_chunk.mp2', 'wb')
|
278
|
+
tmp_data = Tempfile.open('data_chunk')
|
279
|
+
tmp_data.binmode
|
280
|
+
|
281
|
+
remaining = size
|
282
|
+
while (remaining > 0 && !file.eof?)
|
283
|
+
read_bytes = [128, remaining].min
|
284
|
+
tmp_data << file.read(read_bytes)
|
285
|
+
remaining -= read_bytes
|
286
|
+
end
|
287
|
+
tmp_data.rewind
|
288
|
+
chunk = self.new(id, size, tmp_data)
|
289
|
+
|
290
|
+
return chunk
|
291
|
+
end
|
292
|
+
|
293
|
+
def self.new_from_file(file)
|
294
|
+
tmp_data = Tempfile.open('data_chunk')
|
295
|
+
tmp_data.binmode
|
296
|
+
FileUtils.cp(file.path, tmp_data.path)
|
297
|
+
tmp_data.rewind
|
298
|
+
self.new('data', File.size(tmp_data.path).to_s, tmp_data)
|
299
|
+
end
|
300
|
+
|
301
|
+
def initialize(id=nil, size=nil, tmp_data_file=nil)
|
302
|
+
@id, @size, @tmp_data_file = id, size, tmp_data_file
|
303
|
+
end
|
304
|
+
|
305
|
+
def data
|
306
|
+
f = ''
|
307
|
+
if self.tmp_data_file
|
308
|
+
NuWav::WaveFile.log "we have a tmp_data_file!"
|
309
|
+
self.tmp_data_file.rewind
|
310
|
+
f = self.tmp_data_file.read
|
311
|
+
self.tmp_data_file.rewind
|
312
|
+
else
|
313
|
+
NuWav::WaveFile.log "we have NO tmp_data_file!"
|
314
|
+
end
|
315
|
+
f
|
316
|
+
end
|
317
|
+
|
318
|
+
def to_s
|
319
|
+
"<chunk type:data (size:#{data.size})/>"
|
320
|
+
end
|
321
|
+
|
322
|
+
def to_binary
|
323
|
+
NuWav::WaveFile.log "data chunk to_binary"
|
324
|
+
d = self.data
|
325
|
+
NuWav::WaveFile.log "got data size = #{d.size} #{d[0,10]}"
|
326
|
+
out = "data" + write_dword(d.size) + d
|
327
|
+
out
|
328
|
+
end
|
329
|
+
|
330
|
+
end
|
331
|
+
end
|
@@ -0,0 +1,286 @@
|
|
1
|
+
module NuWav
|
2
|
+
|
3
|
+
class WaveFile
|
4
|
+
|
5
|
+
attr_accessor :header, :chunks
|
6
|
+
|
7
|
+
def self.parse(wave_file)
|
8
|
+
NuWav::WaveFile.new.parse(wave_file)
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
self.chunks = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse(wave_file)
|
16
|
+
NuWav::WaveFile.log "Processing wave file #{wave_file.inspect}...."
|
17
|
+
wave_file_size = File.size(wave_file)
|
18
|
+
|
19
|
+
File.open(wave_file, File::RDWR) do |f|
|
20
|
+
|
21
|
+
#only for windows, make sure we are operating in binary mode
|
22
|
+
f.binmode
|
23
|
+
#start at the very beginning, a very good place to start
|
24
|
+
f.seek(0)
|
25
|
+
|
26
|
+
riff, riff_length = read_chunk_header(f)
|
27
|
+
NuWav::WaveFile.log "riff: #{riff}"
|
28
|
+
NuWav::WaveFile.log "riff_length: #{riff_length}"
|
29
|
+
NuWav::WaveFile.log "wave_file_size: #{wave_file_size}"
|
30
|
+
|
31
|
+
raise NotRIFFFormat unless riff == 'RIFF'
|
32
|
+
riff_end = [f.tell + riff_length, wave_file_size].min
|
33
|
+
|
34
|
+
riff_type = f.read(4)
|
35
|
+
raise NotWAVEFormat unless riff_type == 'WAVE'
|
36
|
+
|
37
|
+
@header = RiffChunk.new(riff, riff_length, riff_type)
|
38
|
+
|
39
|
+
while (f.tell + 8) <= riff_end
|
40
|
+
NuWav::WaveFile.log "while #{f.tell} < #{riff_end}"
|
41
|
+
chunk_name, chunk_length = read_chunk_header(f)
|
42
|
+
fpos = f.tell
|
43
|
+
|
44
|
+
NuWav::WaveFile.log "found chunk: '#{chunk_name}', size #{chunk_length}"
|
45
|
+
|
46
|
+
if chunk_name && chunk_length
|
47
|
+
|
48
|
+
self.chunks[chunk_name.to_sym] = chunk_class(chunk_name).parse(chunk_name, chunk_length, f)
|
49
|
+
parsed_chunk_size = self.chunks[chunk_name.to_sym].size
|
50
|
+
|
51
|
+
NuWav::WaveFile.log "about to do a seek..."
|
52
|
+
NuWav::WaveFile.log "f.seek #{fpos} + #{parsed_chunk_size}"
|
53
|
+
f.seek(fpos + parsed_chunk_size)
|
54
|
+
NuWav::WaveFile.log "seek done"
|
55
|
+
|
56
|
+
if parsed_chunk_size.odd?
|
57
|
+
pad = f.read(1)
|
58
|
+
f.seek(fpos + parsed_chunk_size) if (pad.nil? || pad.ord != 0)
|
59
|
+
end
|
60
|
+
|
61
|
+
else
|
62
|
+
NuWav::WaveFile.log "chunk or length was off - remainder of file does not parse properly: #{riff_end} - #{fpos} = #{riff_end - fpos}"
|
63
|
+
f.seek(riff_end)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
@chunks.each{|k,v| NuWav::WaveFile.log "#{k}: #{v}\n\n" unless k.to_s == 'data'}
|
68
|
+
NuWav::WaveFile.log "parse done"
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
def duration
|
73
|
+
fmt = @chunks[:fmt]
|
74
|
+
|
75
|
+
if (PCM_COMPRESSION.include?(fmt.compression_code.to_i))
|
76
|
+
data = @chunks[:data]
|
77
|
+
data.size / (fmt.sample_rate * fmt.number_of_channels * (fmt.sample_bits / 8))
|
78
|
+
elsif (fmt.compression_code.to_i == MPEG_COMPRESSION)
|
79
|
+
# <chunk type:fact samples_number:78695424 />
|
80
|
+
fact = @chunks[:fact]
|
81
|
+
fact.samples_number / fmt.sample_rate
|
82
|
+
else
|
83
|
+
raise "Duration implemented for PCM and MEPG files only."
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def is_mpeg?
|
88
|
+
(@chunks[:fmt] && (@chunks[:fmt].compression_code.to_i == MPEG_COMPRESSION))
|
89
|
+
end
|
90
|
+
|
91
|
+
def is_pcm?
|
92
|
+
(@chunks[:fmt] && (PCM_COMPRESSION.include?(@chunks[:fmt].compression_code.to_i)))
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_s
|
96
|
+
out = "NuWav:#{@header}\n"
|
97
|
+
out = [:fmt, :fact, :mext, :bext, :cart, :data ].inject(out) do |s, chunk|
|
98
|
+
s += "#{self.chunks[chunk]}\n" if self.chunks[chunk]
|
99
|
+
s
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def to_file(file_name, add_extension=false)
|
104
|
+
if add_extension && !(file_name =~ /\.wav/)
|
105
|
+
file_name += ".wav"
|
106
|
+
end
|
107
|
+
NuWav::WaveFile.log "NuWav::WaveFile.to_file: file_name = #{file_name}"
|
108
|
+
|
109
|
+
#get all the chunks together to get final length
|
110
|
+
chunks_out = [:fmt, :fact, :mext, :bext, :cart, :data].inject([]) do |list, chunk|
|
111
|
+
if self.chunks[chunk]
|
112
|
+
out = self.chunks[chunk].to_binary
|
113
|
+
NuWav::WaveFile.log out.length
|
114
|
+
list << out
|
115
|
+
end
|
116
|
+
list
|
117
|
+
end
|
118
|
+
|
119
|
+
# TODO: handle other chunks not in the above list, but that might have been in a parsed wav
|
120
|
+
|
121
|
+
riff_length = chunks_out.inject(0){|sum, chunk| sum += chunk.size}
|
122
|
+
NuWav::WaveFile.log "NuWav::WaveFile.to_file: riff_length = #{riff_length}"
|
123
|
+
|
124
|
+
#open file for writing
|
125
|
+
open(file_name, "wb") do |o|
|
126
|
+
#write the header
|
127
|
+
o << "RIFF"
|
128
|
+
o << [(riff_length + 4)].pack('V')
|
129
|
+
o << "WAVE"
|
130
|
+
#write the chunks
|
131
|
+
chunks_out.each{|c| o << c}
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
def write_data_file(file_name)
|
137
|
+
open(file_name, "wb") do |o|
|
138
|
+
o << chunks[:data].data
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
# method to create a wave file using the
|
144
|
+
def self.from_mpeg(file_name)
|
145
|
+
# read and display infos & tags
|
146
|
+
NuWav::WaveFile.log "NuWav::from_mpeg::file_name:#{file_name}"
|
147
|
+
mp3info = Mp3Info.open(file_name)
|
148
|
+
NuWav::WaveFile.log mp3info
|
149
|
+
file = File.open(file_name)
|
150
|
+
wave = WaveFile.new
|
151
|
+
|
152
|
+
# data chunk
|
153
|
+
data = DataChunk.new_from_file(file)
|
154
|
+
wave.chunks[:data] = data
|
155
|
+
|
156
|
+
# fmt chunk
|
157
|
+
fmt = FmtChunk.new
|
158
|
+
fmt.compression_code = MPEG_COMPRESSION
|
159
|
+
fmt.number_of_channels = (mp3info.channel_mode == "Single Channel") ? 1 : 2
|
160
|
+
fmt.sample_rate = mp3info.samplerate
|
161
|
+
fmt.byte_rate = mp3info.bitrate / 8 * 1000
|
162
|
+
fmt.block_align = calculate_mpeg_frame_size(mp3info)
|
163
|
+
fmt.sample_bits = 65535
|
164
|
+
fmt.extra_size = 22
|
165
|
+
fmt.head_layer = ACM_LAYERS[mp3info.layer.to_i-1]
|
166
|
+
fmt.head_bit_rate = mp3info.bitrate * 1000
|
167
|
+
fmt.head_mode = CHANNEL_MODES[mp3info.channel_mode]
|
168
|
+
# fmt.head_mode_ext = (mp3info.channel_mode == "JStereo") ? 2**mp3info.mode_extension : 0
|
169
|
+
fmt.head_mode_ext = (mp3info.channel_mode == "JStereo") ? 2**mp3info.header[:mode_extension] : 0
|
170
|
+
# fmt.head_emphasis = mp3info.emphasis + 1
|
171
|
+
fmt.head_emphasis = mp3info.header[:emphasis] + 1
|
172
|
+
fmt.head_flags = calculate_mpeg_head_flags(mp3info)
|
173
|
+
fmt.pts_low = 0
|
174
|
+
fmt.pts_high = 0
|
175
|
+
wave.chunks[:fmt] = fmt
|
176
|
+
# NuWav::WaveFile.log "fmt: #{fmt}"
|
177
|
+
|
178
|
+
# fact chunk
|
179
|
+
fact = FactChunk.new
|
180
|
+
fact.samples_number = calculate_mpeg_samples_number(file, mp3info)
|
181
|
+
wave.chunks[:fact] = fact
|
182
|
+
# NuWav::WaveFile.log "fact: #{fact}"
|
183
|
+
|
184
|
+
#mext chunk
|
185
|
+
mext = MextChunk.new
|
186
|
+
mext.sound_information = 5
|
187
|
+
mext.sound_information += 2 if mp3info.header[:padding]
|
188
|
+
mext.frame_size = calculate_mpeg_frame_size(mp3info)
|
189
|
+
mext.ancillary_data_length = 0
|
190
|
+
mext.ancillary_data_def = 0
|
191
|
+
wave.chunks[:mext] = mext
|
192
|
+
# NuWav::WaveFile.log "mext: #{mext}"
|
193
|
+
|
194
|
+
|
195
|
+
#bext chunk
|
196
|
+
bext = BextChunk.new
|
197
|
+
bext.time_reference_high = 0
|
198
|
+
bext.time_reference_low = 0
|
199
|
+
bext.version = 1
|
200
|
+
bext.coding_history = "A=MPEG1L#{mp3info.layer},F=#{mp3info.samplerate},B=#{mp3info.bitrate},M=#{CODING_HISTORY_MODE[mp3info.channel_mode]},T=PRX\r\n\0\0"
|
201
|
+
wave.chunks[:bext] = bext
|
202
|
+
# NuWav::WaveFile.log "bext: #{bext}"
|
203
|
+
|
204
|
+
#cart chunk
|
205
|
+
cart = CartChunk.new
|
206
|
+
now = Time.now
|
207
|
+
today = Date.today
|
208
|
+
later = today << 12
|
209
|
+
cart.version = '0101'
|
210
|
+
cart.title = File.basename(file_name) # this is just a default
|
211
|
+
cart.start_date = today.strftime("%Y-%m-%d")
|
212
|
+
cart.start_time = now.strftime("%H:%M:%S")
|
213
|
+
cart.end_date = later.strftime("%Y-%m-%d")
|
214
|
+
cart.end_time = now.strftime("%H:%M:%S")
|
215
|
+
cart.producer_app_id = 'NuWav'
|
216
|
+
cart.producer_app_version = '1.0'
|
217
|
+
cart.level_reference = 0
|
218
|
+
cart.tag_text = "\r\n"
|
219
|
+
wave.chunks[:cart] = cart
|
220
|
+
# NuWav::WaveFile.log "cart: #{cart}"
|
221
|
+
wave
|
222
|
+
end
|
223
|
+
|
224
|
+
def self.calculate_mpeg_samples_number(file, info)
|
225
|
+
(File.size(file.path) / calculate_mpeg_frame_size(info)) * Mp3Info::SAMPLES_PER_FRAME[info.layer][info.mpeg_version]
|
226
|
+
end
|
227
|
+
|
228
|
+
def self.calculate_mpeg_head_flags(info)
|
229
|
+
flags = 0
|
230
|
+
flags += 1 if (info.header[:private_bit])
|
231
|
+
flags += 2 if (info.header[:copyright])
|
232
|
+
flags += 4 if (info.header[:original])
|
233
|
+
flags += 8 if (info.header[:error_protection])
|
234
|
+
flags += 16 if (info.mpeg_version > 0)
|
235
|
+
flags
|
236
|
+
end
|
237
|
+
|
238
|
+
def self.calculate_mpeg_frame_size(info)
|
239
|
+
samples_per_frame = Mp3Info::SAMPLES_PER_FRAME[info.layer][info.mpeg_version]
|
240
|
+
((samples_per_frame / 8) * (info.bitrate * 1000))/info.samplerate
|
241
|
+
end
|
242
|
+
|
243
|
+
protected
|
244
|
+
|
245
|
+
def read_chunk_header(file)
|
246
|
+
hdr = file.read(8)
|
247
|
+
chunkName, chunkLen = hdr.unpack("A4V") rescue [nil, nil]
|
248
|
+
# NuWav::WaveFile.log "chunkName: '#{chunkName}', chunkLen: '#{chunkLen}'"
|
249
|
+
[chunkName, chunkLen]
|
250
|
+
end
|
251
|
+
|
252
|
+
def chunk_class(name)
|
253
|
+
begin
|
254
|
+
constantize("NuWav::#{camelize("#{name}_chunk")}")
|
255
|
+
rescue NameError
|
256
|
+
NuWav::Chunk
|
257
|
+
end
|
258
|
+
|
259
|
+
end
|
260
|
+
|
261
|
+
# File vendor/rails/activesupport/lib/active_support/inflector.rb, line 147
|
262
|
+
def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
|
263
|
+
if first_letter_in_uppercase
|
264
|
+
lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
|
265
|
+
else
|
266
|
+
lower_case_and_underscored_word.first + camelize(lower_case_and_underscored_word)[1..-1]
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# File vendor/rails/activesupport/lib/active_support/inflector.rb, line 252
|
271
|
+
def constantize(camel_cased_word)
|
272
|
+
unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ camel_cased_word
|
273
|
+
raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!"
|
274
|
+
end
|
275
|
+
Object.module_eval("::#{$1}", __FILE__, __LINE__)
|
276
|
+
end
|
277
|
+
|
278
|
+
def self.log(m)
|
279
|
+
if NuWav::DEBUG
|
280
|
+
puts "#{Time.now}: NuWav: #{m}"
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
end
|
285
|
+
|
286
|
+
end
|