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.
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