File: ogginfo.rb

package info (click to toggle)
libogginfo-ruby 0.4.2-1
  • links: PTS, VCS
  • area: main
  • in suites: squeeze
  • size: 132 kB
  • ctags: 225
  • sloc: ruby: 1,605; makefile: 6
file content (253 lines) | stat: -rw-r--r-- 8,072 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# see http://www.xiph.org/ogg/vorbis/docs.html for documentation on vorbis format
# http://www.xiph.org/vorbis/doc/v-comment.html
# http://www.xiph.org/vorbis/doc/framing.html
# 
# License: ruby

require "iconv"

class Hash 
   ### lets you specify hash["key"] as hash.key
   ### this came from CodingInRuby on RubyGarden
   ### http://www.rubygarden.org/ruby?CodingInRuby 
   def method_missing(meth,*args) 
     if /=$/=~(meth=meth.id2name) then
       self[meth[0...-1]] = (args.length<2 ? args[0] : args)
     else
       self[meth]
     end
   end
end
					    
# Raised on any kind of error related to ruby-ogginfo
class OggInfoError < StandardError ; end

class OggInfo
  VERSION = "0.4.2"
  CHECKSUM_TABLE = [
    0x00000000,0x04c11db7,0x09823b6e,0x0d4326d9,
    0x130476dc,0x17c56b6b,0x1a864db2,0x1e475005,
    0x2608edb8,0x22c9f00f,0x2f8ad6d6,0x2b4bcb61,
    0x350c9b64,0x31cd86d3,0x3c8ea00a,0x384fbdbd,
    0x4c11db70,0x48d0c6c7,0x4593e01e,0x4152fda9,
    0x5f15adac,0x5bd4b01b,0x569796c2,0x52568b75,
    0x6a1936c8,0x6ed82b7f,0x639b0da6,0x675a1011,
    0x791d4014,0x7ddc5da3,0x709f7b7a,0x745e66cd,
    0x9823b6e0,0x9ce2ab57,0x91a18d8e,0x95609039,
    0x8b27c03c,0x8fe6dd8b,0x82a5fb52,0x8664e6e5,
    0xbe2b5b58,0xbaea46ef,0xb7a96036,0xb3687d81,
    0xad2f2d84,0xa9ee3033,0xa4ad16ea,0xa06c0b5d,
    0xd4326d90,0xd0f37027,0xddb056fe,0xd9714b49,
    0xc7361b4c,0xc3f706fb,0xceb42022,0xca753d95,
    0xf23a8028,0xf6fb9d9f,0xfbb8bb46,0xff79a6f1,
    0xe13ef6f4,0xe5ffeb43,0xe8bccd9a,0xec7dd02d,
    0x34867077,0x30476dc0,0x3d044b19,0x39c556ae,
    0x278206ab,0x23431b1c,0x2e003dc5,0x2ac12072,
    0x128e9dcf,0x164f8078,0x1b0ca6a1,0x1fcdbb16,
    0x018aeb13,0x054bf6a4,0x0808d07d,0x0cc9cdca,
    0x7897ab07,0x7c56b6b0,0x71159069,0x75d48dde,
    0x6b93dddb,0x6f52c06c,0x6211e6b5,0x66d0fb02,
    0x5e9f46bf,0x5a5e5b08,0x571d7dd1,0x53dc6066,
    0x4d9b3063,0x495a2dd4,0x44190b0d,0x40d816ba,
    0xaca5c697,0xa864db20,0xa527fdf9,0xa1e6e04e,
    0xbfa1b04b,0xbb60adfc,0xb6238b25,0xb2e29692,
    0x8aad2b2f,0x8e6c3698,0x832f1041,0x87ee0df6,
    0x99a95df3,0x9d684044,0x902b669d,0x94ea7b2a,
    0xe0b41de7,0xe4750050,0xe9362689,0xedf73b3e,
    0xf3b06b3b,0xf771768c,0xfa325055,0xfef34de2,
    0xc6bcf05f,0xc27dede8,0xcf3ecb31,0xcbffd686,
    0xd5b88683,0xd1799b34,0xdc3abded,0xd8fba05a,
    0x690ce0ee,0x6dcdfd59,0x608edb80,0x644fc637,
    0x7a089632,0x7ec98b85,0x738aad5c,0x774bb0eb,
    0x4f040d56,0x4bc510e1,0x46863638,0x42472b8f,
    0x5c007b8a,0x58c1663d,0x558240e4,0x51435d53,
    0x251d3b9e,0x21dc2629,0x2c9f00f0,0x285e1d47,
    0x36194d42,0x32d850f5,0x3f9b762c,0x3b5a6b9b,
    0x0315d626,0x07d4cb91,0x0a97ed48,0x0e56f0ff,
    0x1011a0fa,0x14d0bd4d,0x19939b94,0x1d528623,
    0xf12f560e,0xf5ee4bb9,0xf8ad6d60,0xfc6c70d7,
    0xe22b20d2,0xe6ea3d65,0xeba91bbc,0xef68060b,
    0xd727bbb6,0xd3e6a601,0xdea580d8,0xda649d6f,
    0xc423cd6a,0xc0e2d0dd,0xcda1f604,0xc960ebb3,
    0xbd3e8d7e,0xb9ff90c9,0xb4bcb610,0xb07daba7,
    0xae3afba2,0xaafbe615,0xa7b8c0cc,0xa379dd7b,
    0x9b3660c6,0x9ff77d71,0x92b45ba8,0x9675461f,
    0x8832161a,0x8cf30bad,0x81b02d74,0x857130c3,
    0x5d8a9099,0x594b8d2e,0x5408abf7,0x50c9b640,
    0x4e8ee645,0x4a4ffbf2,0x470cdd2b,0x43cdc09c,
    0x7b827d21,0x7f436096,0x7200464f,0x76c15bf8,
    0x68860bfd,0x6c47164a,0x61043093,0x65c52d24,
    0x119b4be9,0x155a565e,0x18197087,0x1cd86d30,
    0x029f3d35,0x065e2082,0x0b1d065b,0x0fdc1bec,
    0x3793a651,0x3352bbe6,0x3e119d3f,0x3ad08088,
    0x2497d08d,0x2056cd3a,0x2d15ebe3,0x29d4f654,
    0xc5a92679,0xc1683bce,0xcc2b1d17,0xc8ea00a0,
    0xd6ad50a5,0xd26c4d12,0xdf2f6bcb,0xdbee767c,
    0xe3a1cbc1,0xe760d676,0xea23f0af,0xeee2ed18,
    0xf0a5bd1d,0xf464a0aa,0xf9278673,0xfde69bc4,
    0x89b8fd09,0x8d79e0be,0x803ac667,0x84fbdbd0,
    0x9abc8bd5,0x9e7d9662,0x933eb0bb,0x97ffad0c,
    0xafb010b1,0xab710d06,0xa6322bdf,0xa2f33668,
    0xbcb4666d,0xb8757bda,0xb5365d03,0xb1f740b4 
  ]

  attr_reader :channels, :samplerate, :bitrate, :nominal_bitrate, :length
  
  # +tag+ is a hash containing the vorbis tag like "Artist", "Title", and the like
  attr_reader :tag

  # create new instance of OggInfo, using +charset+ to convert tags to
  def initialize(filename, charset = "utf-8")
    @filename = filename
    @charset = charset
    @file = File.new(@filename, "rb")

    frames = (1..2).collect { |i| OggInfo.read_frame(@file) }
    extract_vorbis_infos(frames[0])
    extract_tag(frames[1])
    convert_tag_charset("utf-8", @charset)
    @original_tag = @tag.dup
    @length = get_length
    @bitrate = @file.stat.size.to_f*8/@length
    @file.close
  end

  # "block version" of ::new()
  def self.open(*args)
    m = self.new(*args)
    ret = nil
    if block_given?
      begin
        ret = yield(m)
      ensure
        m.close
      end
    else
      ret = m
    end
    ret
  end

  # commits any tags to file
  def close
    if @tag != @original_tag
      cmd = %w{vorbiscomment -w} 
      convert_tag_charset(@charset, "utf-8")

      @tag.each do |k,v|
        cmd.concat(["-t", k.upcase+"="+v])
      end
      cmd << @filename
      system(*cmd)
    end
  end

  # check the presence of a tag
  def hastag?
    !@tag.empty?
  end
  
  def to_s
    "channels #{@channels} samplerate #{@samplerate} bitrate #{@nominal_bitrate} bitrate #{@bitrate} length #{@length} #{@tag.inspect}"
  end

  # read an ogg frame from the +file+
  def self.read_frame(file)
    frame = {}

    frame[:file_position] = file.pos

    return nil if file.eof?
    chunk = file.read(27)
    raise OggInfoError if file.eof?

    capture_pattern,
    frame[:version],
    frame[:header_type], 
    frame[:granule_pos], 
    frame[:bitstream_serial_number], 
    frame[:page_sequence_number],
    frame[:checksum],
    frame[:page_segments] = chunk.unpack("a4CCQNNNC")

    if capture_pattern != "OggS"
      raise(OggInfoError, "bad magic number '#{capture_pattern}'")
    end
    
    segment_sizes = file.read(frame[:page_segments]).unpack("C*")
    frame[:body_size] = segment_sizes.inject(0) { |sum, i| sum += i }
    frame[:header_size] = 27 + frame[:page_segments]
    frame[:size] = frame[:header_size] + frame[:body_size]
    file.seek(frame[:body_size], IO::SEEK_CUR)
    frame
  end

  # compute the checksum of a given +frame+ from a given +file+, you can compare it with frame[:checksum].
  def self.checksum(file, frame)
    original_pos = file.pos
    file.seek(frame[:file_position])
    data = file.read(frame[:size])
    data[22] = data[23] = data[24] = data[25] = 0

    crc = 0
    data.each_byte do |byte|
      crc = (crc << 8)^CHECKSUM_TABLE[((crc >> 24)&0xff) ^ byte]
      crc = crc & 0xffffffff
    end

    # "reverse" it
    crc = [crc].pack("V").unpack("N").first
    file.seek(original_pos)
    crc
  end

private
  def extract_vorbis_infos(frame)
    @file.seek(frame[:file_position] + frame[:header_size] + 1 ) # seek after "vorbis"
    vorbis_string, vorbis_version, @channels, @samplerate, upper_bitrate, @nominal_bitrate, lower_bitrate = @file.read(27).unpack("a6VCV4")
    if @nominal_bitrate == 0
      if upper_bitrate == 2**32 - 1 or lower_bitrate == 2**32 - 1 
	@nominal_bitrate = 0
      else
	@nominal_bitrate = (upper_bitrate + lower_bitrate)/2
      end
    end
  end

  def extract_tag(frame)
    @file.seek(frame[:file_position] + frame[:header_size] + 1 + "vorbis".size)
    vendor_length = @file.read(4).unpack("V").first
    @vendor = @file.read(vendor_length)

    @tag = {}
    tag_size = @file.read(4).unpack("V")[0]

    tag_size.times do |i|
      size = @file.read(4).unpack("V")[0]
      comment = @file.read(size)
      key, val = comment.split(/=/, 2)
      @tag[key.downcase] = val
    end
  end

  def get_length
    @file.seek(0)
    last_frame = nil
    begin
      while f = OggInfo.read_frame(@file) 
        last_frame = f
      end
    rescue OggInfoError
    end
    last_frame[:granule_pos].to_f / @samplerate
  end

  def convert_tag_charset(from_charset, to_charset)
    return if from_charset == to_charset
    Iconv.open(to_charset, from_charset) do |ic|
      @tag.each do |k, v|
        @tag[k] = ic.iconv(v)
      end
    end
  end
end