wmainfo-rb 0.4 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README +2 -1
- data/lib/wmainfo.rb +113 -112
- metadata +2 -2
data/README
CHANGED
@@ -35,5 +35,6 @@ For more/different documentation see http://badcomputer.org/unix/code/wmainfo/
|
|
35
35
|
|
36
36
|
== Thanks/Contributors ==
|
37
37
|
|
38
|
-
Ilmari Heikkinen sent in a fix for uninitialized '@ext_info'
|
38
|
+
Ilmari Heikkinen sent in a fix for uninitialized '@ext_info'.
|
39
|
+
Guillaume Pierronnet sent in a patch which improves character encoding handling.
|
39
40
|
|
data/lib/wmainfo.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# = Description
|
2
2
|
#
|
3
|
-
# wmainfo-ruby gives you access to low level information on wma/wmv files.
|
3
|
+
# wmainfo-ruby gives you access to low level information on wma/wmv/asf files.
|
4
4
|
# * It identifies all "ASF_..." objects and shows each objects size
|
5
5
|
# * It returns info such as bitrate, size, length, creation date etc
|
6
6
|
# * It returns meta-tags from ASF_Content_Description_Object
|
@@ -10,14 +10,17 @@
|
|
10
10
|
# I wrestled with the ASF spec (150 page .doc format!) with no joy for
|
11
11
|
# a while, then found Dan Sully's Audio-WMA Perl module:
|
12
12
|
# (http://cpants.perl.org/dist/Audio-WMA :: http://www.slimdevices.com/)
|
13
|
-
# This entire library is essentially a translation of WMA.pm
|
13
|
+
# This entire library is essentially a translation of (parts of) WMA.pm
|
14
14
|
# to Ruby. All credit for the hard work is owed to Dan...
|
15
15
|
#
|
16
16
|
# License:: Ruby
|
17
17
|
# Author:: Darren Kirby (mailto:bulliver@badcomputer.org)
|
18
18
|
# Website:: http://badcomputer.org/unix/code/wmainfo/
|
19
19
|
|
20
|
-
#
|
20
|
+
# Improved character encoding handling thanks to
|
21
|
+
# Guillaume Pierronnet <guillaume.pierronnet @nospam@ gmail.com>
|
22
|
+
|
23
|
+
require 'iconv'
|
21
24
|
|
22
25
|
# raised when errors occur parsing wma header
|
23
26
|
class WmaInfoError < StandardError
|
@@ -25,23 +28,24 @@ end
|
|
25
28
|
|
26
29
|
class WmaInfo
|
27
30
|
# WmaInfo.tags and WmaInfo.info are hashes of NAME=VALUE pairs
|
28
|
-
# WmaInfo.
|
29
|
-
attr_reader :tags, :
|
30
|
-
def initialize(file,
|
31
|
+
# WmaInfo.header_object is a hash of arrays
|
32
|
+
attr_reader :tags, :header_object, :info, :stream
|
33
|
+
def initialize(file, opts = {})
|
31
34
|
@drm = nil
|
32
35
|
@tags = {}
|
33
|
-
@
|
36
|
+
@header_object = {}
|
34
37
|
@info = {}
|
35
38
|
@ext_info = {}
|
36
39
|
@file = file
|
37
|
-
@debug = debug
|
38
|
-
|
40
|
+
@debug = opts[:debug]
|
41
|
+
@ic = Iconv.new(opts[:encoding] || "ASCII", "UTF-16LE")
|
42
|
+
parse_wma_header
|
39
43
|
end
|
40
44
|
|
41
45
|
# for ASF_Header_Object prints: "name: GUID size num_objects"
|
42
46
|
# for others, prints: "name: GUID size offset"
|
43
47
|
def print_objects
|
44
|
-
@
|
48
|
+
@header_object.each_pair do |key,val|
|
45
49
|
puts "#{key}: #{val[0]} #{val[1]} #{val[2]}"
|
46
50
|
end
|
47
51
|
end
|
@@ -77,8 +81,8 @@ class WmaInfo
|
|
77
81
|
def parse_stream
|
78
82
|
begin
|
79
83
|
@stream = {}
|
80
|
-
offset = @
|
81
|
-
|
84
|
+
offset = @header_object['ASF_Stream_Properties_Object'][2]
|
85
|
+
parse_asf_stream_properties_object(offset)
|
82
86
|
rescue
|
83
87
|
raise WmaInfoError, "Cannot grok ASF_Stream_Properties_Object", caller
|
84
88
|
end
|
@@ -94,7 +98,7 @@ class WmaInfo
|
|
94
98
|
# This cleans up the output when using WmaInfo in irb
|
95
99
|
def inspect #:nodoc:
|
96
100
|
s = "#<#{self.class}:0x#{(self.object_id*2).to_s(16)} "
|
97
|
-
@
|
101
|
+
@header_object.each_pair do |k,v|
|
98
102
|
s += "(#{k.upcase} size=#{v[1]} offset=#{v[2]}) " unless k == "ASF_Header_Object"
|
99
103
|
end
|
100
104
|
s += "\b>"
|
@@ -102,80 +106,78 @@ class WmaInfo
|
|
102
106
|
#++
|
103
107
|
|
104
108
|
private
|
105
|
-
def
|
109
|
+
def parse_wma_header
|
106
110
|
@size = File.size(@file)
|
107
111
|
@fh = File.new(@file, "rb")
|
108
112
|
@offset = 0
|
109
|
-
@
|
110
|
-
@
|
111
|
-
@
|
112
|
-
require 'iconv'
|
113
|
-
@ic = Iconv.new("ASCII//IGNORE", "UTF-16LE")
|
113
|
+
@file_offset = 30
|
114
|
+
@guid_mapping = known_guids
|
115
|
+
@reverse_guid_mapping = @guid_mapping.invert
|
114
116
|
|
115
117
|
# read first 30 bytes and parse ASF_Header_Object
|
116
118
|
begin
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
reserved1
|
121
|
-
reserved2
|
122
|
-
|
119
|
+
object_id = byte_string_to_guid(@fh.read(16))
|
120
|
+
object_size = @fh.read(8).unpack("V")[0]
|
121
|
+
header_objects = @fh.read(4).unpack("V")[0]
|
122
|
+
reserved1 = @fh.read(1).unpack("b*")[0]
|
123
|
+
reserved2 = @fh.read(1).unpack("b*")[0]
|
124
|
+
object_id_name = @reverse_guid_mapping[object_id]
|
123
125
|
rescue
|
124
126
|
# not getting raised when fed a non-wma file
|
125
|
-
#
|
127
|
+
# object_size must be getting value because
|
126
128
|
# "Header size reported larger than file size"
|
127
129
|
# gets raised instead?
|
128
130
|
raise WmaInfoError, "Not a wma header", caller
|
129
131
|
end
|
130
132
|
|
131
|
-
if
|
133
|
+
if object_size > @size
|
132
134
|
raise WmaInfoError, "Header size reported larger than file size", caller
|
133
135
|
end
|
134
136
|
|
135
|
-
@
|
137
|
+
@header_object[object_id_name] = [object_id, object_size, header_objects, reserved1, reserved2]
|
136
138
|
|
137
139
|
if @debug
|
138
|
-
puts "
|
139
|
-
puts "
|
140
|
-
puts "
|
141
|
-
puts "
|
142
|
-
puts "reserved1:
|
143
|
-
puts "reserved2:
|
140
|
+
puts "object_id: #{object_id}"
|
141
|
+
puts "object_id_name: #{@reverse_guid_mapping[object_id]}"
|
142
|
+
puts "object_size: #{object_size}"
|
143
|
+
puts "header_objects: #{header_objects}"
|
144
|
+
puts "reserved1: #{reserved1}"
|
145
|
+
puts "reserved2: #{reserved2}"
|
144
146
|
end
|
145
147
|
|
146
|
-
@
|
148
|
+
@header_data = @fh.read(object_size - 30)
|
147
149
|
@fh.close
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
150
|
+
header_objects.times do
|
151
|
+
next_object = read_and_increment_offset(16)
|
152
|
+
next_object_text = byte_string_to_guid(next_object)
|
153
|
+
next_object_size = parse_64bit_string(read_and_increment_offset(8))
|
154
|
+
next_object_name = @reverse_guid_mapping[next_object_text];
|
153
155
|
|
154
|
-
@
|
155
|
-
@
|
156
|
+
@header_object[next_object_name] = [next_object_text, next_object_size, @file_offset]
|
157
|
+
@file_offset += next_object_size
|
156
158
|
|
157
159
|
if @debug
|
158
|
-
puts "
|
159
|
-
puts "
|
160
|
-
puts "
|
160
|
+
puts "next_objectGUID: #{next_object_text}"
|
161
|
+
puts "next_object_name: #{next_object_name}"
|
162
|
+
puts "next_object_size: #{next_object_size}"
|
161
163
|
end
|
162
164
|
|
163
165
|
# start looking at object contents
|
164
|
-
if
|
165
|
-
|
166
|
+
if next_object_name == 'ASF_File_Properties_Object'
|
167
|
+
parse_asf_file_properties_object
|
166
168
|
next
|
167
|
-
elsif
|
168
|
-
|
169
|
+
elsif next_object_name == 'ASF_Content_Description_Object'
|
170
|
+
parse_asf_content_description_object
|
169
171
|
next
|
170
|
-
elsif
|
171
|
-
|
172
|
+
elsif next_object_name == 'ASF_Extended_Content_Description_Object'
|
173
|
+
parse_asf_extended_content_description_object
|
172
174
|
next
|
173
|
-
elsif
|
174
|
-
|
175
|
+
elsif next_object_name == 'ASF_Content_Encryption_Object' || next_object_name == 'ASF_Extended_Content_Encryption_Object'
|
176
|
+
parse_asf_content_encryption_object
|
175
177
|
end
|
176
178
|
|
177
179
|
# set our next object size
|
178
|
-
@offset +=
|
180
|
+
@offset += next_object_size - 24
|
179
181
|
end
|
180
182
|
|
181
183
|
# meta-tag like values go to 'tags' all others to 'info'
|
@@ -191,23 +193,23 @@ class WmaInfo
|
|
191
193
|
@tags.delete_if { |k,v| v == "" || v == nil }
|
192
194
|
end
|
193
195
|
|
194
|
-
def
|
196
|
+
def parse_asf_content_encryption_object
|
195
197
|
@drm = 1
|
196
198
|
end
|
197
199
|
|
198
|
-
def
|
199
|
-
fileid =
|
200
|
-
@info['fileid_guid'] =
|
201
|
-
@info['filesize'] =
|
202
|
-
@info['creation_date'] =
|
203
|
-
@info['creation_date_unix'] =
|
200
|
+
def parse_asf_file_properties_object
|
201
|
+
fileid = read_and_increment_offset(16)
|
202
|
+
@info['fileid_guid'] = byte_string_to_guid(fileid)
|
203
|
+
@info['filesize'] = parse_64bit_string(read_and_increment_offset(8))
|
204
|
+
@info['creation_date'] = read_and_increment_offset(8).unpack("Q")[0]
|
205
|
+
@info['creation_date_unix'] = file_time_to_unix_time(@info['creation_date'])
|
204
206
|
@info['creation_string'] = Time.at(@info['creation_date_unix'].to_i)
|
205
|
-
@info['data_packets'] =
|
206
|
-
@info['play_duration'] =
|
207
|
-
@info['send_duration'] =
|
208
|
-
@info['preroll'] =
|
207
|
+
@info['data_packets'] = read_and_increment_offset(8).unpack("V")[0]
|
208
|
+
@info['play_duration'] = parse_64bit_string(read_and_increment_offset(8))
|
209
|
+
@info['send_duration'] = parse_64bit_string(read_and_increment_offset(8))
|
210
|
+
@info['preroll'] = read_and_increment_offset(8).unpack("V")[0]
|
209
211
|
@info['playtime_seconds'] = (@info['play_duration'] / 10000000) - (@info['preroll'] / 1000)
|
210
|
-
flags_raw =
|
212
|
+
flags_raw = read_and_increment_offset(4).unpack("V")[0]
|
211
213
|
if flags_raw & 0x0001 == 0
|
212
214
|
@info['broadcast'] = 0
|
213
215
|
else
|
@@ -218,9 +220,9 @@ class WmaInfo
|
|
218
220
|
else
|
219
221
|
@info['seekable'] = 1
|
220
222
|
end
|
221
|
-
@info['min_packet_size'] =
|
222
|
-
@info['max_packet_size'] =
|
223
|
-
@info['max_bitrate'] =
|
223
|
+
@info['min_packet_size'] = read_and_increment_offset(4).unpack("V")[0]
|
224
|
+
@info['max_packet_size'] = read_and_increment_offset(4).unpack("V")[0]
|
225
|
+
@info['max_bitrate'] = read_and_increment_offset(4).unpack("V")[0]
|
224
226
|
@info['bitrate'] = @info['max_bitrate'] / 1000
|
225
227
|
|
226
228
|
if @debug
|
@@ -229,36 +231,36 @@ class WmaInfo
|
|
229
231
|
|
230
232
|
end
|
231
233
|
|
232
|
-
def
|
234
|
+
def parse_asf_content_description_object
|
233
235
|
lengths = {}
|
234
236
|
keys = %w/Title Author Copyright Description Rating/
|
235
237
|
keys.each do |key| # read the lengths of each key
|
236
|
-
lengths[key] =
|
238
|
+
lengths[key] = read_and_increment_offset(2).unpack("v")[0]
|
237
239
|
end
|
238
240
|
keys.each do |key| # now pull the data based on length
|
239
|
-
@tags[key] =
|
241
|
+
@tags[key] = decode_binary_string(read_and_increment_offset(lengths[key]))
|
240
242
|
end
|
241
243
|
end
|
242
244
|
|
243
|
-
def
|
245
|
+
def parse_asf_extended_content_description_object
|
244
246
|
@ext_info = {}
|
245
|
-
@ext_info['content_count'] =
|
247
|
+
@ext_info['content_count'] = read_and_increment_offset(2).unpack("v")[0]
|
246
248
|
@ext_info['content_count'].times do |n|
|
247
249
|
ext = {}
|
248
250
|
ext['base_offset'] = @offset + 30
|
249
|
-
ext['name_length'] =
|
250
|
-
ext['name'] =
|
251
|
-
ext['value_type'] =
|
252
|
-
ext['value_length'] =
|
251
|
+
ext['name_length'] = read_and_increment_offset(2).unpack("v")[0]
|
252
|
+
ext['name'] = decode_binary_string(read_and_increment_offset(ext['name_length']))
|
253
|
+
ext['value_type'] = read_and_increment_offset(2).unpack("v")[0]
|
254
|
+
ext['value_length'] = read_and_increment_offset(2).unpack("v")[0]
|
253
255
|
|
254
|
-
value =
|
256
|
+
value = read_and_increment_offset(ext['value_length'])
|
255
257
|
if ext['value_type'] <= 1
|
256
|
-
ext['value'] =
|
258
|
+
ext['value'] = decode_binary_string(value)
|
257
259
|
elsif ext['value_type'] == 4
|
258
|
-
ext['value'] =
|
260
|
+
ext['value'] = parse_64bit_string(value)
|
259
261
|
else
|
260
|
-
|
261
|
-
ext['value'] = value.unpack(
|
262
|
+
value_type_template = ["", "", "V", "V", "", "v"]
|
263
|
+
ext['value'] = value.unpack(value_type_template[ext['value_type']])[0]
|
262
264
|
end
|
263
265
|
|
264
266
|
if @debug
|
@@ -274,35 +276,35 @@ class WmaInfo
|
|
274
276
|
end
|
275
277
|
end
|
276
278
|
|
277
|
-
def
|
279
|
+
def parse_asf_stream_properties_object(offset)
|
278
280
|
@offset = offset - 6 # gained an extra 6 bytes somewhere?!
|
279
281
|
|
280
|
-
streamType =
|
281
|
-
@stream['stream_type_guid'] =
|
282
|
-
@stream['stream_type_name'] = @
|
283
|
-
errorType =
|
284
|
-
@stream['error_correct_guid'] =
|
285
|
-
@stream['error_correct_name'] = @
|
286
|
-
|
287
|
-
@stream['time_offset'] =
|
288
|
-
@stream['type_data_length'] =
|
289
|
-
@stream['error_data_length'] =
|
290
|
-
flags_raw =
|
282
|
+
streamType = read_and_increment_offset(16)
|
283
|
+
@stream['stream_type_guid'] = byte_string_to_guid(streamType)
|
284
|
+
@stream['stream_type_name'] = @reverse_guid_mapping[@stream['stream_type_guid']]
|
285
|
+
errorType = read_and_increment_offset(16)
|
286
|
+
@stream['error_correct_guid'] = byte_string_to_guid(errorType)
|
287
|
+
@stream['error_correct_name'] = @reverse_guid_mapping[@stream['error_correct_guid']]
|
288
|
+
|
289
|
+
@stream['time_offset'] = read_and_increment_offset(8).unpack("4v")[0]
|
290
|
+
@stream['type_data_length'] = read_and_increment_offset(4).unpack("2v")[0]
|
291
|
+
@stream['error_data_length'] = read_and_increment_offset(4).unpack("2v")[0]
|
292
|
+
flags_raw = read_and_increment_offset(2).unpack("v")[0]
|
291
293
|
@stream['stream_number'] = flags_raw & 0x007F
|
292
294
|
@stream['encrypted'] = flags_raw & 0x8000
|
293
295
|
|
294
|
-
#
|
295
|
-
|
296
|
+
# reserved - set to zero
|
297
|
+
read_and_increment_offset(4)
|
296
298
|
|
297
|
-
@stream['type_specific_data'] =
|
298
|
-
@stream['error_correct_data'] =
|
299
|
+
@stream['type_specific_data'] = read_and_increment_offset(@stream['type_data_length'])
|
300
|
+
@stream['error_correct_data'] = read_and_increment_offset(@stream['error_data_length'])
|
299
301
|
|
300
302
|
if @stream['stream_type_name'] == 'ASF_Audio_Media'
|
301
|
-
|
303
|
+
parse_asf_audio_media_object
|
302
304
|
end
|
303
305
|
end
|
304
306
|
|
305
|
-
def
|
307
|
+
def parse_asf_audio_media_object
|
306
308
|
data = @stream['type_specific_data'][0...16]
|
307
309
|
@stream['audio_channels'] = data[2...4].unpack("v")[0]
|
308
310
|
@stream['audio_sample_rate'] = data[4...8].unpack("2v")[0]
|
@@ -310,19 +312,18 @@ class WmaInfo
|
|
310
312
|
@stream['audio_bits_per_sample'] = data[14...16].unpack("v")[0]
|
311
313
|
end
|
312
314
|
|
313
|
-
# UTF16LE -> ASCII
|
314
|
-
def
|
315
|
-
|
316
|
-
textString.sub!(/\x00$/, '')
|
315
|
+
# UTF16LE -> ASCII
|
316
|
+
def decode_binary_string(data)
|
317
|
+
@ic.iconv(data).strip
|
317
318
|
end
|
318
319
|
|
319
|
-
def
|
320
|
-
value = @
|
320
|
+
def read_and_increment_offset(size)
|
321
|
+
value = @header_data[@offset...(@offset + size)]
|
321
322
|
@offset += size
|
322
323
|
return value
|
323
324
|
end
|
324
325
|
|
325
|
-
def
|
326
|
+
def byte_string_to_guid(byteString)
|
326
327
|
guidString = sprintf("%02X", byteString[3])
|
327
328
|
guidString += sprintf("%02X", byteString[2])
|
328
329
|
guidString += sprintf("%02X", byteString[1])
|
@@ -345,17 +346,17 @@ class WmaInfo
|
|
345
346
|
guidString += sprintf("%02X", byteString[15])
|
346
347
|
end
|
347
348
|
|
348
|
-
def
|
349
|
+
def parse_64bit_string(data)
|
349
350
|
d = data.unpack('VV')
|
350
351
|
d[1] * 2 ** 32 + d[0]
|
351
352
|
end
|
352
353
|
|
353
|
-
def
|
354
|
+
def file_time_to_unix_time(time)
|
354
355
|
(time - 116444736000000000) / 10000000
|
355
356
|
end
|
356
357
|
|
357
|
-
def
|
358
|
-
|
358
|
+
def known_guids
|
359
|
+
guid_mapping = {
|
359
360
|
'ASF_Extended_Stream_Properties_Object' => '14E6A5CB-C672-4332-8399-A96952065B5A',
|
360
361
|
'ASF_Padding_Object' => '1806D474-CADF-4509-A4BA-9AABCB96AAE8',
|
361
362
|
'ASF_Payload_Ext_Syst_Pixel_Aspect_Ratio' => '1B1EE554-F9EA-4BC8-821A-376B74E4C4B8',
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.4
|
|
3
3
|
specification_version: 1
|
4
4
|
name: wmainfo-rb
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: "0.
|
7
|
-
date:
|
6
|
+
version: "0.5"
|
7
|
+
date: 2008-03-18 00:00:00 -06:00
|
8
8
|
summary: Pure Ruby lib for accessing info/tags from wma/wmv files
|
9
9
|
require_paths:
|
10
10
|
- lib
|