wmainfo-rb 0.4 → 0.5
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/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
|