wmainfo-rb 0.4 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README +2 -1
  2. data/lib/wmainfo.rb +113 -112
  3. 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
 
@@ -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
- # TODO: Get rid of all the CamelCase
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.headerObject is a hash of arrays
29
- attr_reader :tags, :headerObject, :info, :stream
30
- def initialize(file, debug=nil)
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
- @headerObject = {}
36
+ @header_object = {}
34
37
  @info = {}
35
38
  @ext_info = {}
36
39
  @file = file
37
- @debug = debug
38
- parseWmaHeader
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
- @headerObject.each_pair do |key,val|
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 = @headerObject['ASF_Stream_Properties_Object'][2]
81
- parseASFStreamPropertiesObject(offset)
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
- @headerObject.each_pair do |k,v|
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 parseWmaHeader
109
+ def parse_wma_header
106
110
  @size = File.size(@file)
107
111
  @fh = File.new(@file, "rb")
108
112
  @offset = 0
109
- @fileOffset = 30
110
- @guidMapping = knownGUIDs
111
- @reverseGuidMapping = @guidMapping.invert
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
- objectId = byteStringToGUID(@fh.read(16))
118
- objectSize = @fh.read(8).unpack("V")[0]
119
- headerObjects = @fh.read(4).unpack("V")[0]
120
- reserved1 = @fh.read(1).unpack("b*")[0]
121
- reserved2 = @fh.read(1).unpack("b*")[0]
122
- objectIdName = @reverseGuidMapping[objectId]
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
- # objectSize must be getting value because
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 objectSize > @size
133
+ if object_size > @size
132
134
  raise WmaInfoError, "Header size reported larger than file size", caller
133
135
  end
134
136
 
135
- @headerObject[objectIdName] = [objectId, objectSize, headerObjects, reserved1, reserved2]
137
+ @header_object[object_id_name] = [object_id, object_size, header_objects, reserved1, reserved2]
136
138
 
137
139
  if @debug
138
- puts "objectId: #{objectId}"
139
- puts "objectIdName: #{@reverseGuidMapping[objectId]}"
140
- puts "objectSize: #{objectSize}"
141
- puts "headerObjects: #{headerObjects}"
142
- puts "reserved1: #{reserved1}"
143
- puts "reserved2: #{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
- @headerData = @fh.read(objectSize - 30)
148
+ @header_data = @fh.read(object_size - 30)
147
149
  @fh.close
148
- headerObjects.times do
149
- nextObject = readAndIncrementOffset(16)
150
- nextObjectText = byteStringToGUID(nextObject)
151
- nextObjectSize = parse64BitString(readAndIncrementOffset(8))
152
- nextObjectName = @reverseGuidMapping[nextObjectText];
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
- @headerObject[nextObjectName] = [nextObjectText, nextObjectSize, @fileOffset]
155
- @fileOffset += nextObjectSize
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 "nextObjectGUID: #{nextObjectText}"
159
- puts "nextObjectName: #{nextObjectName}"
160
- puts "nextObjectSize: #{nextObjectSize}"
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 nextObjectName == 'ASF_File_Properties_Object'
165
- parseASFFilePropertiesObject
166
+ if next_object_name == 'ASF_File_Properties_Object'
167
+ parse_asf_file_properties_object
166
168
  next
167
- elsif nextObjectName == 'ASF_Content_Description_Object'
168
- parseASFContentDescriptionObject
169
+ elsif next_object_name == 'ASF_Content_Description_Object'
170
+ parse_asf_content_description_object
169
171
  next
170
- elsif nextObjectName == 'ASF_Extended_Content_Description_Object'
171
- parseASFExtendedContentDescriptionObject
172
+ elsif next_object_name == 'ASF_Extended_Content_Description_Object'
173
+ parse_asf_extended_content_description_object
172
174
  next
173
- elsif nextObjectName == 'ASF_Content_Encryption_Object' || nextObjectName == 'ASF_Extended_Content_Encryption_Object'
174
- parseASFContentEncryptionObject
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 += nextObjectSize - 24
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 parseASFContentEncryptionObject
196
+ def parse_asf_content_encryption_object
195
197
  @drm = 1
196
198
  end
197
199
 
198
- def parseASFFilePropertiesObject
199
- fileid = readAndIncrementOffset(16)
200
- @info['fileid_guid'] = byteStringToGUID(fileid)
201
- @info['filesize'] = parse64BitString(readAndIncrementOffset(8))
202
- @info['creation_date'] = readAndIncrementOffset(8).unpack("Q")[0]
203
- @info['creation_date_unix'] = fileTimeToUnixTime(@info['creation_date'])
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'] = readAndIncrementOffset(8).unpack("V")[0]
206
- @info['play_duration'] = parse64BitString(readAndIncrementOffset(8))
207
- @info['send_duration'] = parse64BitString(readAndIncrementOffset(8))
208
- @info['preroll'] = readAndIncrementOffset(8).unpack("V")[0]
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 = readAndIncrementOffset(4).unpack("V")[0]
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'] = readAndIncrementOffset(4).unpack("V")[0]
222
- @info['max_packet_size'] = readAndIncrementOffset(4).unpack("V")[0]
223
- @info['max_bitrate'] = readAndIncrementOffset(4).unpack("V")[0]
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 parseASFContentDescriptionObject
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] = readAndIncrementOffset(2).unpack("v")[0]
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] = decodeBinaryString(readAndIncrementOffset(lengths[key]))
241
+ @tags[key] = decode_binary_string(read_and_increment_offset(lengths[key]))
240
242
  end
241
243
  end
242
244
 
243
- def parseASFExtendedContentDescriptionObject
245
+ def parse_asf_extended_content_description_object
244
246
  @ext_info = {}
245
- @ext_info['content_count'] = readAndIncrementOffset(2).unpack("v")[0]
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'] = readAndIncrementOffset(2).unpack("v")[0]
250
- ext['name'] = decodeBinaryString(readAndIncrementOffset(ext['name_length']))
251
- ext['value_type'] = readAndIncrementOffset(2).unpack("v")[0]
252
- ext['value_length'] = readAndIncrementOffset(2).unpack("v")[0]
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 = readAndIncrementOffset(ext['value_length'])
256
+ value = read_and_increment_offset(ext['value_length'])
255
257
  if ext['value_type'] <= 1
256
- ext['value'] = decodeBinaryString(value)
258
+ ext['value'] = decode_binary_string(value)
257
259
  elsif ext['value_type'] == 4
258
- ext['value'] = parse64BitString(value)
260
+ ext['value'] = parse_64bit_string(value)
259
261
  else
260
- valTypeTemplates = ["", "", "V", "V", "", "v"]
261
- ext['value'] = value.unpack(valTypeTemplates[ext['value_type']])[0]
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 parseASFStreamPropertiesObject(offset)
279
+ def parse_asf_stream_properties_object(offset)
278
280
  @offset = offset - 6 # gained an extra 6 bytes somewhere?!
279
281
 
280
- streamType = readAndIncrementOffset(16)
281
- @stream['stream_type_guid'] = byteStringToGUID(streamType)
282
- @stream['stream_type_name'] = @reverseGuidMapping[@stream['stream_type_guid']]
283
- errorType = readAndIncrementOffset(16)
284
- @stream['error_correct_guid'] = byteStringToGUID(errorType)
285
- @stream['error_correct_name'] = @reverseGuidMapping[@stream['error_correct_guid']]
286
-
287
- @stream['time_offset'] = readAndIncrementOffset(8).unpack("4v")[0]
288
- @stream['type_data_length'] = readAndIncrementOffset(4).unpack("2v")[0]
289
- @stream['error_data_length'] = readAndIncrementOffset(4).unpack("2v")[0]
290
- flags_raw = readAndIncrementOffset(2).unpack("v")[0]
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
- # reserved - set to zero
295
- readAndIncrementOffset(4)
296
+ # reserved - set to zero
297
+ read_and_increment_offset(4)
296
298
 
297
- @stream['type_specific_data'] = readAndIncrementOffset(@stream['type_data_length'])
298
- @stream['error_correct_data'] = readAndIncrementOffset(@stream['error_data_length'])
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
- parseASFAudioMediaObject
303
+ parse_asf_audio_media_object
302
304
  end
303
305
  end
304
306
 
305
- def parseASFAudioMediaObject
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 ... am still not happy with this
314
- def decodeBinaryString(data)
315
- textString = @ic.iconv(data[0, data.length / 2 * 2] + "\000\000")[0..-2]
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 readAndIncrementOffset(size)
320
- value = @headerData[@offset..(@offset + size)]
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 byteStringToGUID(byteString)
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 parse64BitString(data)
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 fileTimeToUnixTime(time)
354
+ def file_time_to_unix_time(time)
354
355
  (time - 116444736000000000) / 10000000
355
356
  end
356
357
 
357
- def knownGUIDs
358
- guidMapping = {
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.4"
7
- date: 2007-10-09 00:00:00 -06:00
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