ruby-ogginfo 0.5 → 0.6.5

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,11 @@
1
+ === 0.6.5 / 2011-04-07
2
+
3
+ * internal reorganization, leading to more robust and faster library
4
+
5
+ === 0.6 / 2011-03-01
6
+
7
+ * pure ruby tag writing (thanks to Grant Gardner)
8
+
1
9
  === 0.5 / 2011-01-13
2
10
 
3
11
  * speex support (thanks to Grant Gardner)
data/Manifest.txt CHANGED
@@ -2,6 +2,14 @@ History.txt
2
2
  Manifest.txt
3
3
  README.rdoc
4
4
  Rakefile
5
- setup.rb
5
+ lib/ogg/codecs/comments.rb
6
+ lib/ogg/codecs/speex.rb
7
+ lib/ogg/codecs/vorbis.rb
8
+ lib/ogg/page.rb
9
+ lib/ogg/reader.rb
10
+ lib/ogg/writer.rb
11
+ lib/ogg.rb
6
12
  lib/ogginfo.rb
13
+ setup.rb
7
14
  test/test_ruby-ogginfo.rb
15
+ test/test_ruby-spxinfo.rb
data/Rakefile CHANGED
@@ -4,19 +4,13 @@ require 'rubygems'
4
4
  require 'hoe'
5
5
 
6
6
  Hoe.plugin :yard
7
+ Hoe.plugin :git
8
+ Hoe.plugin :rcov
7
9
 
8
- require 'lib/ogginfo.rb'
9
-
10
- Hoe.new('ruby-ogginfo', OggInfo::VERSION) do |p|
11
- p.rubyforge_name = 'ruby-ogginfo'
12
- p.author = 'Guillaume Pierronnet'
13
- p.email = 'moumar@rubyforge.org'
14
- p.summary = 'ruby-ogginfo is a pure-ruby library that gives low level informations on ogg files'
15
- p.description = p.paragraphs_of('README.rdoc', 3).first
16
- p.url = p.paragraphs_of('README.rdoc', 1).first
17
- p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
18
- p.remote_rdoc_dir = ''
19
- p.rdoc_locations << "rubyforge.org:/var/www/gforge-projects/ruby-ogginfo/"
10
+ Hoe.spec('ruby-ogginfo') do
11
+ developer('Guillaume Pierronnet','moumar@rubyforge.org')
12
+ developer('Grant Gardner','grant@lastweekend.com.au')
13
+ summary = 'ruby-ogginfo is a pure-ruby library that gives low level informations on ogg files'
20
14
  end
21
15
 
22
16
  # vim: syntax=Ruby
@@ -0,0 +1,50 @@
1
+ require "stringio"
2
+
3
+ module Ogg::Codecs
4
+ # See http://www.xiph.org/vorbis/doc/v-comment.html
5
+ # Methods to pack/unpack vorbis comment packets
6
+ # intended to be included into Codec classes
7
+ module VorbisComments
8
+ # unpack a packet, skipping the preamble
9
+ # returns a 2 element array being a Hash of tag/value pairs and the vendor string
10
+ def unpack_comments(packet, preamble="")
11
+ pio = StringIO.new(packet)
12
+ pio.read(preamble.length)
13
+
14
+ vendor_length = pio.read(4).unpack("V").first
15
+ vendor = pio.read(vendor_length)
16
+
17
+ tag = {}
18
+ tag_size = pio.read(4).unpack("V")[0]
19
+
20
+ tag_size.times do |i|
21
+ size = pio.read(4).unpack("V")[0]
22
+ comment = pio.read(size)
23
+ key, val = comment.split(/=/, 2)
24
+ tag[key.downcase] = val
25
+ end
26
+
27
+ #framing bit = pio.read(1).unpack("C")[0]
28
+ [ tag, vendor ]
29
+ end
30
+
31
+ # Pack tag Hash and vendor string into an ogg packet.
32
+ def pack_comments(tag, vendor, preamble="")
33
+ packet_data = ""
34
+ packet_data << preamble
35
+
36
+ packet_data << [ vendor.length ].pack("V")
37
+ packet_data << vendor
38
+
39
+ packet_data << [tag.size].pack("V")
40
+ tag.each do |k,v|
41
+ tag_data = "#{ k }=#{ v }"
42
+ packet_data << [ tag_data.length ].pack("V")
43
+ packet_data << tag_data
44
+ end
45
+
46
+ packet_data << "\001"
47
+ packet_data
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,40 @@
1
+ module Ogg::Codecs
2
+ class Speex
3
+ class << self
4
+ include VorbisComments
5
+
6
+ def match?(packet)
7
+ /^Speex/ =~ packet
8
+ end
9
+
10
+ def decode_headers(reader)
11
+ init_packet, tag_packet = reader.read_packets(2)
12
+ info = extract_info(init_packet)
13
+ info[:tag], info[:tag_vendor] = unpack_comments(tag_packet)
14
+ return info
15
+ end
16
+
17
+ def replace_tags(reader, writer, new_tags, vendor)
18
+ tag_packet = reader.read_packets(1)
19
+ writer.write_packets(0, pack_comments(new_tags, vendor))
20
+ end
21
+
22
+ def extract_info(info_packet)
23
+ speex_string,
24
+ speex_version,
25
+ speex_version_id,
26
+ header_size,
27
+ samplerate,
28
+ mode,
29
+ mode_bitstream_version,
30
+ channels,
31
+ nominal_bitrate,
32
+ framesize,
33
+ vbr = info_packet.unpack("A8A20VVVVVVVVV")
34
+ #not sure how to make sense of the bitrate info,picard doesn't show it either...
35
+
36
+ return { :channels => channels, :samplerate => samplerate, :nominal_bitrate => nominal_bitrate }
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,46 @@
1
+ module Ogg::Codecs
2
+ class Vorbis
3
+ class << self
4
+ include VorbisComments
5
+ # return true/false based on whether the header packet belongs to us
6
+ def match?(header_packet)
7
+ /^\001vorbis.*/ =~ header_packet
8
+ end
9
+
10
+ #consume header and tag pages, return array of two hashes, info and tags
11
+ def decode_headers(reader)
12
+ init_pkt, tag_pkt, setup_pkt = reader.read_packets(3)
13
+ info = extract_info(init_pkt)
14
+ info[:tag], info[:tag_vendor] = unpack_comments(tag_pkt, "\003vorbis")
15
+ info
16
+ end
17
+
18
+ # consume pages with old tags/setup packets and rewrite newtags,setup packets
19
+ # return the number of pages written
20
+ def replace_tags(reader, writer, new_tags, vendor)
21
+ tag_pkt, setup_pkt = reader.read_packets(2)
22
+ writer.write_packets(0, pack_comments(new_tags, vendor, "\003vorbis"), setup_pkt)
23
+ end
24
+
25
+ def extract_info(packet)
26
+ vorbis_string,
27
+ vorbis_version,
28
+ channels,
29
+ samplerate,
30
+ upper_bitrate,
31
+ nominal_bitrate,
32
+ lower_bitrate = packet.unpack("a7VCV4")
33
+
34
+ if nominal_bitrate == 0
35
+ if (upper_bitrate == 2**32 - 1) || (lower_bitrate == 2**32 - 1)
36
+ nominal_bitrate = 0
37
+ else
38
+ nominal_bitrate = ( upper_bitrate + lower_bitrate) / 2
39
+ end
40
+ end
41
+
42
+ return { :channels => channels, :samplerate => samplerate, :nominal_bitrate => nominal_bitrate }
43
+ end
44
+ end
45
+ end
46
+ end
data/lib/ogg/page.rb ADDED
@@ -0,0 +1,78 @@
1
+ module Ogg
2
+ class Page
3
+ attr_accessor :granule_pos, :bitstream_serial_no, :sequence_no, :segments, :header
4
+ attr_reader :checksum
5
+
6
+ # read an ogg frame from the +file+
7
+ # file must be positioned at end of frame after this loop
8
+ # options - :skip_body = seek to end of frame rather than reading in the data
9
+ def self.read(io, options = {})
10
+ return nil if io.eof?
11
+
12
+ chunk = io.read(27)
13
+
14
+ capture_pattern,
15
+ version,
16
+ header,
17
+ granule_pos,
18
+ bitstream_serial_no,
19
+ sequence_no,
20
+ @checksum,
21
+ segments = chunk.unpack("a4CCQVVVC") #a4CCQNNNC
22
+
23
+ if capture_pattern != "OggS"
24
+ raise(StreamError, "bad magic number '#{ capture_pattern }'")
25
+ end
26
+
27
+ page = Page.new(bitstream_serial_no, granule_pos)
28
+ page.header = header
29
+ page.sequence_no = sequence_no
30
+ raise(StreamError, "got EOF when reading page") if io.eof?
31
+
32
+ segment_sizes = io.read(segments).unpack("C*")
33
+ if options[:skip_body]
34
+ body_size = segment_sizes.inject(0) { |sum, i| sum + i }
35
+ io.seek(body_size, IO::SEEK_CUR)
36
+ else
37
+ segment_sizes.each do |size|
38
+ break if io.eof?
39
+ page.segments << io.read(size)
40
+ end
41
+ if options[:checksum]
42
+ if @checksum != Ogg.compute_checksum(page.pack)
43
+ raise(StreamError, "bad checksum: expected #{ @checksum }, got #{ page.checksum }")
44
+ end
45
+ end
46
+ end
47
+
48
+ page
49
+ end
50
+
51
+ def initialize(bitstream_serial_no = 0, granule_pos = 0)
52
+ @bitstream_serial_no = bitstream_serial_no
53
+ @granule_pos = granule_pos
54
+ @segments = []
55
+ @header = 0
56
+ end
57
+
58
+ def pack
59
+ packed = [
60
+ "OggS",
61
+ 0, #version
62
+ @header,
63
+ @granule_pos,
64
+ @bitstream_serial_no,
65
+ @sequence_no,
66
+ 0, #checksum
67
+ @segments.length
68
+ ].pack("a4CCQVVVC")
69
+
70
+ packed << @segments.collect { |segment| segment.length }.pack("C*")
71
+ packed << @segments.join
72
+ crc = Ogg.compute_checksum(packed)
73
+ packed[22..25] = [crc].pack("V")
74
+ packed
75
+ end
76
+
77
+ end
78
+ end
data/lib/ogg/reader.rb ADDED
@@ -0,0 +1,37 @@
1
+ module Ogg
2
+ #Reads pages and packets from an ogg stream
3
+ class Reader
4
+ attr_reader :input
5
+
6
+ def initialize(input)
7
+ @input = input
8
+ end
9
+
10
+ def each_pages(options = {})
11
+ until @input.eof?
12
+ yield Page.read(@input, options)
13
+ end
14
+ end
15
+
16
+ def read_packets(max_packets)
17
+ result = []
18
+ partial_packet = ""
19
+ each_pages do |page|
20
+ partial_packet = page.segments.inject(partial_packet) do |packet,segment|
21
+ packet << segment
22
+ if segment.length < 255
23
+ #end of packet
24
+ result << packet
25
+ return result if result.length == max_packets
26
+ ""
27
+ else
28
+ packet
29
+ end
30
+ end
31
+ end
32
+ # We expect packets to reach page boundaries, consider raising exception if partial_packet here.
33
+ result
34
+ end
35
+ end
36
+
37
+ end
data/lib/ogg/writer.rb ADDED
@@ -0,0 +1,48 @@
1
+ require 'stringio'
2
+
3
+ module Ogg
4
+ # Writes pages or packets to an output io
5
+
6
+ class Writer
7
+ attr_reader :output
8
+
9
+ def initialize(bitstream_serial_no, output)
10
+ @output = output
11
+ @page_sequence = 0
12
+ @bitstream_serial_no = bitstream_serial_no
13
+ end
14
+
15
+ # Writes a page to the output, the serial number and page sequence are
16
+ # are overwritten to be appropriate for this stream.
17
+ def write_page(page)
18
+ page.sequence_no = @page_sequence
19
+ @output << page.pack
20
+ @page_sequence += 1
21
+ end
22
+
23
+ def write_packets(granule_pos, *packets)
24
+ written_pages_count = 1
25
+ page = Page.new(@bitstream_serial_no, granule_pos)
26
+ packets.each do |packet|
27
+ io = StringIO.new(packet)
28
+
29
+ while !io.eof? do
30
+ page.segments << io.read(255)
31
+ if (page.segments.length == 255)
32
+ page.granule_pos = -1
33
+ write_page(page)
34
+ page = Page.new(@bitstream_serial_no, granule_pos)
35
+ written_pages_count += 1
36
+ end
37
+ end
38
+ #If our packet was an exact multiple of 255 we need to put in an empty closing segment
39
+ if (page.segments.length == 0 || page.segments.last.length == 255)
40
+ page.segments << ""
41
+ end
42
+ end
43
+ #we always need to flush the final page.
44
+ write_page(page)
45
+ written_pages_count
46
+ end
47
+ end
48
+ end
data/lib/ogg.rb ADDED
@@ -0,0 +1,139 @@
1
+ #Ogg framing
2
+ # see http://www.xiph.org/ogg/vorbis/docs.html for documentation on vorbis format
3
+ # http://www.xiph.org/vorbis/doc/framing.html
4
+
5
+ %w{page reader writer codecs/comments codecs/vorbis codecs/speex}.each do |file|
6
+ require File.join(File.dirname(__FILE__), "ogg", file)
7
+ end
8
+
9
+ module Ogg
10
+ # Raised on any kind of Ogg parsing/writing error
11
+ class StreamError < StandardError; end
12
+ CHECKSUM_TABLE = [
13
+ 0x00000000,0x04c11db7,0x09823b6e,0x0d4326d9,
14
+ 0x130476dc,0x17c56b6b,0x1a864db2,0x1e475005,
15
+ 0x2608edb8,0x22c9f00f,0x2f8ad6d6,0x2b4bcb61,
16
+ 0x350c9b64,0x31cd86d3,0x3c8ea00a,0x384fbdbd,
17
+ 0x4c11db70,0x48d0c6c7,0x4593e01e,0x4152fda9,
18
+ 0x5f15adac,0x5bd4b01b,0x569796c2,0x52568b75,
19
+ 0x6a1936c8,0x6ed82b7f,0x639b0da6,0x675a1011,
20
+ 0x791d4014,0x7ddc5da3,0x709f7b7a,0x745e66cd,
21
+ 0x9823b6e0,0x9ce2ab57,0x91a18d8e,0x95609039,
22
+ 0x8b27c03c,0x8fe6dd8b,0x82a5fb52,0x8664e6e5,
23
+ 0xbe2b5b58,0xbaea46ef,0xb7a96036,0xb3687d81,
24
+ 0xad2f2d84,0xa9ee3033,0xa4ad16ea,0xa06c0b5d,
25
+ 0xd4326d90,0xd0f37027,0xddb056fe,0xd9714b49,
26
+ 0xc7361b4c,0xc3f706fb,0xceb42022,0xca753d95,
27
+ 0xf23a8028,0xf6fb9d9f,0xfbb8bb46,0xff79a6f1,
28
+ 0xe13ef6f4,0xe5ffeb43,0xe8bccd9a,0xec7dd02d,
29
+ 0x34867077,0x30476dc0,0x3d044b19,0x39c556ae,
30
+ 0x278206ab,0x23431b1c,0x2e003dc5,0x2ac12072,
31
+ 0x128e9dcf,0x164f8078,0x1b0ca6a1,0x1fcdbb16,
32
+ 0x018aeb13,0x054bf6a4,0x0808d07d,0x0cc9cdca,
33
+ 0x7897ab07,0x7c56b6b0,0x71159069,0x75d48dde,
34
+ 0x6b93dddb,0x6f52c06c,0x6211e6b5,0x66d0fb02,
35
+ 0x5e9f46bf,0x5a5e5b08,0x571d7dd1,0x53dc6066,
36
+ 0x4d9b3063,0x495a2dd4,0x44190b0d,0x40d816ba,
37
+ 0xaca5c697,0xa864db20,0xa527fdf9,0xa1e6e04e,
38
+ 0xbfa1b04b,0xbb60adfc,0xb6238b25,0xb2e29692,
39
+ 0x8aad2b2f,0x8e6c3698,0x832f1041,0x87ee0df6,
40
+ 0x99a95df3,0x9d684044,0x902b669d,0x94ea7b2a,
41
+ 0xe0b41de7,0xe4750050,0xe9362689,0xedf73b3e,
42
+ 0xf3b06b3b,0xf771768c,0xfa325055,0xfef34de2,
43
+ 0xc6bcf05f,0xc27dede8,0xcf3ecb31,0xcbffd686,
44
+ 0xd5b88683,0xd1799b34,0xdc3abded,0xd8fba05a,
45
+ 0x690ce0ee,0x6dcdfd59,0x608edb80,0x644fc637,
46
+ 0x7a089632,0x7ec98b85,0x738aad5c,0x774bb0eb,
47
+ 0x4f040d56,0x4bc510e1,0x46863638,0x42472b8f,
48
+ 0x5c007b8a,0x58c1663d,0x558240e4,0x51435d53,
49
+ 0x251d3b9e,0x21dc2629,0x2c9f00f0,0x285e1d47,
50
+ 0x36194d42,0x32d850f5,0x3f9b762c,0x3b5a6b9b,
51
+ 0x0315d626,0x07d4cb91,0x0a97ed48,0x0e56f0ff,
52
+ 0x1011a0fa,0x14d0bd4d,0x19939b94,0x1d528623,
53
+ 0xf12f560e,0xf5ee4bb9,0xf8ad6d60,0xfc6c70d7,
54
+ 0xe22b20d2,0xe6ea3d65,0xeba91bbc,0xef68060b,
55
+ 0xd727bbb6,0xd3e6a601,0xdea580d8,0xda649d6f,
56
+ 0xc423cd6a,0xc0e2d0dd,0xcda1f604,0xc960ebb3,
57
+ 0xbd3e8d7e,0xb9ff90c9,0xb4bcb610,0xb07daba7,
58
+ 0xae3afba2,0xaafbe615,0xa7b8c0cc,0xa379dd7b,
59
+ 0x9b3660c6,0x9ff77d71,0x92b45ba8,0x9675461f,
60
+ 0x8832161a,0x8cf30bad,0x81b02d74,0x857130c3,
61
+ 0x5d8a9099,0x594b8d2e,0x5408abf7,0x50c9b640,
62
+ 0x4e8ee645,0x4a4ffbf2,0x470cdd2b,0x43cdc09c,
63
+ 0x7b827d21,0x7f436096,0x7200464f,0x76c15bf8,
64
+ 0x68860bfd,0x6c47164a,0x61043093,0x65c52d24,
65
+ 0x119b4be9,0x155a565e,0x18197087,0x1cd86d30,
66
+ 0x029f3d35,0x065e2082,0x0b1d065b,0x0fdc1bec,
67
+ 0x3793a651,0x3352bbe6,0x3e119d3f,0x3ad08088,
68
+ 0x2497d08d,0x2056cd3a,0x2d15ebe3,0x29d4f654,
69
+ 0xc5a92679,0xc1683bce,0xcc2b1d17,0xc8ea00a0,
70
+ 0xd6ad50a5,0xd26c4d12,0xdf2f6bcb,0xdbee767c,
71
+ 0xe3a1cbc1,0xe760d676,0xea23f0af,0xeee2ed18,
72
+ 0xf0a5bd1d,0xf464a0aa,0xf9278673,0xfde69bc4,
73
+ 0x89b8fd09,0x8d79e0be,0x803ac667,0x84fbdbd0,
74
+ 0x9abc8bd5,0x9e7d9662,0x933eb0bb,0x97ffad0c,
75
+ 0xafb010b1,0xab710d06,0xa6322bdf,0xa2f33668,
76
+ 0xbcb4666d,0xb8757bda,0xb5365d03,0xb1f740b4
77
+ ]
78
+
79
+
80
+ class << self
81
+ def detect_codec(input)
82
+ if input.kind_of?(Page)
83
+ first_page = input
84
+ else
85
+ first_page = Page.read(input)
86
+ input.rewind
87
+ end
88
+
89
+ codecs = Ogg::Codecs.constants.map { |module_name| Ogg::Codecs.class_eval(module_name) }.select { |c| c.is_a?(Class) }
90
+ codec = codecs.detect { |c| c.match?(first_page.segments.first) }
91
+ unless codec
92
+ raise(StreamError,"unknown codec")
93
+ end
94
+
95
+ return codec
96
+ end
97
+
98
+ # Calculate the checksum from the page (or the pre packed data)
99
+ # If data it supplied it will be updated to record the checksum value
100
+ def compute_checksum(data_)
101
+ data = data_.dup
102
+ data[22..25] = [0].pack("V")
103
+ crc = 0
104
+
105
+ data.each_byte do |byte|
106
+ crc = (crc << 8)^CHECKSUM_TABLE[((crc >> 24)&0xff) ^ byte]
107
+ crc = crc & 0xffffffff
108
+ end
109
+ crc
110
+ end
111
+ end
112
+
113
+ end
114
+
115
+ if __FILE__ == $0
116
+ require 'pp'
117
+ infile = ARGV[0]
118
+ outfile = ARGV[1]
119
+
120
+ vendor = ""
121
+ tags = { }
122
+ File.open(infile,"r") do |input|
123
+ info = Ogg.read_headers(input)
124
+ pp info
125
+ vendor = info[:tag_vendor]
126
+ tag = info[:tag]
127
+ end
128
+
129
+ File.open(infile,"r") do | input |
130
+ File.open(outfile,"w") do | output |
131
+ Ogg.replace_tags(input,output,tags,vendor)
132
+ end
133
+ end
134
+
135
+ File.open(outfile,"r") do | output |
136
+ pp Ogg.read_headers(output)
137
+ end
138
+
139
+ end
data/lib/ogginfo.rb CHANGED
@@ -5,6 +5,9 @@
5
5
  # License: ruby
6
6
 
7
7
  require "iconv"
8
+ require 'forwardable'
9
+ require "tempfile"
10
+ require File.join(File.dirname(__FILE__), 'ogg.rb')
8
11
 
9
12
  class Hash
10
13
  ### lets you specify hash["key"] as hash.key
@@ -23,95 +26,59 @@ end
23
26
  class OggInfoError < StandardError ; end
24
27
 
25
28
  class OggInfo
26
- VERSION = "0.5"
27
- CHECKSUM_TABLE = [
28
- 0x00000000,0x04c11db7,0x09823b6e,0x0d4326d9,
29
- 0x130476dc,0x17c56b6b,0x1a864db2,0x1e475005,
30
- 0x2608edb8,0x22c9f00f,0x2f8ad6d6,0x2b4bcb61,
31
- 0x350c9b64,0x31cd86d3,0x3c8ea00a,0x384fbdbd,
32
- 0x4c11db70,0x48d0c6c7,0x4593e01e,0x4152fda9,
33
- 0x5f15adac,0x5bd4b01b,0x569796c2,0x52568b75,
34
- 0x6a1936c8,0x6ed82b7f,0x639b0da6,0x675a1011,
35
- 0x791d4014,0x7ddc5da3,0x709f7b7a,0x745e66cd,
36
- 0x9823b6e0,0x9ce2ab57,0x91a18d8e,0x95609039,
37
- 0x8b27c03c,0x8fe6dd8b,0x82a5fb52,0x8664e6e5,
38
- 0xbe2b5b58,0xbaea46ef,0xb7a96036,0xb3687d81,
39
- 0xad2f2d84,0xa9ee3033,0xa4ad16ea,0xa06c0b5d,
40
- 0xd4326d90,0xd0f37027,0xddb056fe,0xd9714b49,
41
- 0xc7361b4c,0xc3f706fb,0xceb42022,0xca753d95,
42
- 0xf23a8028,0xf6fb9d9f,0xfbb8bb46,0xff79a6f1,
43
- 0xe13ef6f4,0xe5ffeb43,0xe8bccd9a,0xec7dd02d,
44
- 0x34867077,0x30476dc0,0x3d044b19,0x39c556ae,
45
- 0x278206ab,0x23431b1c,0x2e003dc5,0x2ac12072,
46
- 0x128e9dcf,0x164f8078,0x1b0ca6a1,0x1fcdbb16,
47
- 0x018aeb13,0x054bf6a4,0x0808d07d,0x0cc9cdca,
48
- 0x7897ab07,0x7c56b6b0,0x71159069,0x75d48dde,
49
- 0x6b93dddb,0x6f52c06c,0x6211e6b5,0x66d0fb02,
50
- 0x5e9f46bf,0x5a5e5b08,0x571d7dd1,0x53dc6066,
51
- 0x4d9b3063,0x495a2dd4,0x44190b0d,0x40d816ba,
52
- 0xaca5c697,0xa864db20,0xa527fdf9,0xa1e6e04e,
53
- 0xbfa1b04b,0xbb60adfc,0xb6238b25,0xb2e29692,
54
- 0x8aad2b2f,0x8e6c3698,0x832f1041,0x87ee0df6,
55
- 0x99a95df3,0x9d684044,0x902b669d,0x94ea7b2a,
56
- 0xe0b41de7,0xe4750050,0xe9362689,0xedf73b3e,
57
- 0xf3b06b3b,0xf771768c,0xfa325055,0xfef34de2,
58
- 0xc6bcf05f,0xc27dede8,0xcf3ecb31,0xcbffd686,
59
- 0xd5b88683,0xd1799b34,0xdc3abded,0xd8fba05a,
60
- 0x690ce0ee,0x6dcdfd59,0x608edb80,0x644fc637,
61
- 0x7a089632,0x7ec98b85,0x738aad5c,0x774bb0eb,
62
- 0x4f040d56,0x4bc510e1,0x46863638,0x42472b8f,
63
- 0x5c007b8a,0x58c1663d,0x558240e4,0x51435d53,
64
- 0x251d3b9e,0x21dc2629,0x2c9f00f0,0x285e1d47,
65
- 0x36194d42,0x32d850f5,0x3f9b762c,0x3b5a6b9b,
66
- 0x0315d626,0x07d4cb91,0x0a97ed48,0x0e56f0ff,
67
- 0x1011a0fa,0x14d0bd4d,0x19939b94,0x1d528623,
68
- 0xf12f560e,0xf5ee4bb9,0xf8ad6d60,0xfc6c70d7,
69
- 0xe22b20d2,0xe6ea3d65,0xeba91bbc,0xef68060b,
70
- 0xd727bbb6,0xd3e6a601,0xdea580d8,0xda649d6f,
71
- 0xc423cd6a,0xc0e2d0dd,0xcda1f604,0xc960ebb3,
72
- 0xbd3e8d7e,0xb9ff90c9,0xb4bcb610,0xb07daba7,
73
- 0xae3afba2,0xaafbe615,0xa7b8c0cc,0xa379dd7b,
74
- 0x9b3660c6,0x9ff77d71,0x92b45ba8,0x9675461f,
75
- 0x8832161a,0x8cf30bad,0x81b02d74,0x857130c3,
76
- 0x5d8a9099,0x594b8d2e,0x5408abf7,0x50c9b640,
77
- 0x4e8ee645,0x4a4ffbf2,0x470cdd2b,0x43cdc09c,
78
- 0x7b827d21,0x7f436096,0x7200464f,0x76c15bf8,
79
- 0x68860bfd,0x6c47164a,0x61043093,0x65c52d24,
80
- 0x119b4be9,0x155a565e,0x18197087,0x1cd86d30,
81
- 0x029f3d35,0x065e2082,0x0b1d065b,0x0fdc1bec,
82
- 0x3793a651,0x3352bbe6,0x3e119d3f,0x3ad08088,
83
- 0x2497d08d,0x2056cd3a,0x2d15ebe3,0x29d4f654,
84
- 0xc5a92679,0xc1683bce,0xcc2b1d17,0xc8ea00a0,
85
- 0xd6ad50a5,0xd26c4d12,0xdf2f6bcb,0xdbee767c,
86
- 0xe3a1cbc1,0xe760d676,0xea23f0af,0xeee2ed18,
87
- 0xf0a5bd1d,0xf464a0aa,0xf9278673,0xfde69bc4,
88
- 0x89b8fd09,0x8d79e0be,0x803ac667,0x84fbdbd0,
89
- 0x9abc8bd5,0x9e7d9662,0x933eb0bb,0x97ffad0c,
90
- 0xafb010b1,0xab710d06,0xa6322bdf,0xa2f33668,
91
- 0xbcb4666d,0xb8757bda,0xb5365d03,0xb1f740b4
92
- ]
93
-
94
- attr_reader :channels, :samplerate, :bitrate, :nominal_bitrate, :length
29
+ VERSION = "0.6.5"
30
+ extend Forwardable
31
+ include Ogg
32
+
33
+ attr_reader :channels, :samplerate, :nominal_bitrate
95
34
 
96
35
  # +tag+ is a hash containing the vorbis tag like "Artist", "Title", and the like
97
36
  attr_reader :tag
98
-
99
- # create new instance of OggInfo, using +charset+ to convert tags to
37
+
38
+ # create new instance of OggInfo, using +charset+ to convert tags
100
39
  def initialize(filename, charset = "utf-8")
101
40
  @filename = filename
102
41
  @charset = charset
103
- @file = File.new(@filename, "rb")
42
+ @length = nil
43
+ @bitrate = nil
44
+ filesize = File.size(@filename)
45
+ File.open(@filename) do |file|
46
+ begin
47
+ info = read_headers(file)
48
+ @samplerate = info[:samplerate]
49
+ @nominal_bitrate = info[:nominal_bitrate]
50
+ @channels = info[:channels]
51
+ @tag = info[:tag]
52
+ # filesize is used to calculate bitrate
53
+ # but we don't want to include the headers
54
+ @filesize = file.stat.size - file.pos
55
+ rescue Ogg::StreamError => se
56
+ raise(OggInfoError, se.message, se.backtrace)
57
+ end
58
+ end
104
59
 
105
- frames = (1..2).collect { |i| OggInfo.read_frame(@file) }
106
- extract_bitstream_infos(frames[0])
107
- extract_tag(frames[1])
108
60
  convert_tag_charset("utf-8", @charset)
109
61
  @original_tag = @tag.dup
110
- @length = get_length
111
- @bitrate = @file.stat.size.to_f*8/@length
112
- @file.close
113
62
  end
114
63
 
64
+ # The length in seconds of the track
65
+ # since this requires reading the whole file we only get it
66
+ # if called
67
+ def length
68
+ unless @length
69
+ File.open(@filename) do |file|
70
+ @length = compute_length(file)
71
+ end
72
+ end
73
+ return @length
74
+ end
75
+
76
+ # Calculated bit rate, also lazily loaded
77
+ # since we depend on the length
78
+ def bitrate
79
+ @bitrate ||= (@filesize * 8).to_f / length()
80
+ end
81
+
115
82
  # "block version" of ::new()
116
83
  def self.open(*args)
117
84
  m = self.new(*args)
@@ -130,148 +97,87 @@ class OggInfo
130
97
 
131
98
  # commits any tags to file
132
99
  def close
133
- if @bitstream_format == "vorbis" && @tag != @original_tag
134
- cmd = %w{vorbiscomment -w}
100
+ if tag != @original_tag
135
101
  convert_tag_charset(@charset, "utf-8")
136
-
137
- @tag.each do |k,v|
138
- cmd.concat(["-t", k.upcase+"="+v])
102
+
103
+ tempfile = Tempfile.new("ruby-ogginfo")
104
+ begin
105
+ File.open(@filename, "rb") do | input |
106
+ replace_tags(input, tempfile, tag)
107
+ end
108
+ tempfile.close
109
+ FileUtils.cp(tempfile.path, @filename)
110
+ ensure
111
+ tempfile.close!
139
112
  end
140
- cmd << @filename
141
- system(*cmd)
142
113
  end
143
114
  end
144
115
 
145
116
  # check the presence of a tag
146
117
  def hastag?
147
- !@tag.empty?
118
+ !tag.empty?
148
119
  end
149
120
 
150
121
  def to_s
151
- "channels #{@channels} samplerate #{@samplerate} bitrate #{@nominal_bitrate} bitrate #{@bitrate} length #{@length} #{@tag.inspect}"
152
- end
153
-
154
- # read an ogg frame from the +file+
155
- def self.read_frame(file)
156
- frame = {}
157
-
158
- frame[:file_position] = file.pos
159
-
160
- return nil if file.eof?
161
- chunk = file.read(27)
162
- raise OggInfoError if file.eof?
163
-
164
- capture_pattern,
165
- frame[:version],
166
- frame[:header_type],
167
- frame[:granule_pos],
168
- frame[:bitstream_serial_number],
169
- frame[:page_sequence_number],
170
- frame[:checksum],
171
- frame[:page_segments] = chunk.unpack("a4CCQNNNC")
172
-
173
- if capture_pattern != "OggS"
174
- raise(OggInfoError, "bad magic number '#{capture_pattern}'")
175
- end
176
-
177
- segment_sizes = file.read(frame[:page_segments]).unpack("C*")
178
- frame[:body_size] = segment_sizes.inject(0) { |sum, i| sum += i }
179
- frame[:header_size] = 27 + frame[:page_segments]
180
- frame[:size] = frame[:header_size] + frame[:body_size]
181
- file.seek(frame[:body_size], IO::SEEK_CUR)
182
- frame
183
- end
184
-
185
- # compute the checksum of a given +frame+ from a given +file+, you can compare it with frame[:checksum].
186
- def self.checksum(file, frame)
187
- original_pos = file.pos
188
- file.seek(frame[:file_position])
189
- data = file.read(frame[:size])
190
- data[22] = data[23] = data[24] = data[25] = 0
191
-
192
- crc = 0
193
- data.each_byte do |byte|
194
- crc = (crc << 8)^CHECKSUM_TABLE[((crc >> 24)&0xff) ^ byte]
195
- crc = crc & 0xffffffff
196
- end
197
-
198
- # "reverse" it
199
- crc = [crc].pack("V").unpack("N").first
200
- file.seek(original_pos)
201
- crc
122
+ "channels #{channels} samplerate #{samplerate} bitrate #{nominal_bitrate} #{tag.inspect}"
202
123
  end
203
124
 
204
125
  private
205
- def extract_bitstream_infos(frame)
206
- @file.seek(frame[:file_position] + frame[:header_size] + 1)
207
- @bitstream_format = @file.read(6).rstrip()
208
- case @bitstream_format
209
- when "vorbis" : extract_vorbis_infos(frame)
210
- when "peex" :
211
- #Speex does not align itself to a 4 byte boundary.
212
- @bitstream_format="Speex"
213
- extract_speex_infos(frame)
214
- else raise(OggInfoError,"Unknown bitstream format #{@bitstream_format}")
215
- end
216
- end
217
126
 
218
- def extract_speex_infos(frame)
219
- @file.seek(frame[:file_position] + frame[:header_size] )
220
- speex_string,speex_version,speex_version_id,header_size,@samplerate,mode,mode_bitstream_version,
221
- @channels,bitrate,framesize,vbr = @file.read(28+ 9 * 4).unpack("A8A20VVVVVVVVV")
222
- #not sure how to make sense of the bitrate info,picard doesn't show it either...
127
+ def read_headers(input)
128
+ reader = Reader.new(input)
129
+ codec = Ogg.detect_codec(input)
130
+ codec.decode_headers(reader)
223
131
  end
224
-
225
- def extract_vorbis_infos(frame)
226
- @file.seek(frame[:file_position] + frame[:header_size] + 1 )
227
-
228
- vorbis_string, vorbis_version, @channels, @samplerate, upper_bitrate, @nominal_bitrate, lower_bitrate = @file.read(27).unpack("a6VCV4")
229
- if @nominal_bitrate == 0
230
- if upper_bitrate == 2**32 - 1 or lower_bitrate == 2**32 - 1
231
- @nominal_bitrate = 0
232
- else
233
- @nominal_bitrate = (upper_bitrate + lower_bitrate)/2
234
- end
235
- end
132
+
133
+ # For both Vorbis and Speex, the granule_pos is the number of samples
134
+ # strictly this should be a codec function.
135
+ def compute_length(input)
136
+ reader = Reader.new(input)
137
+ last_page = nil
138
+ reader.each_pages({ :skip_body => true, :skip_checksum => true }) { |page| last_page = page }
139
+ return last_page.granule_pos.to_f / @samplerate
236
140
  end
141
+
237
142
 
238
- def extract_tag(frame)
239
- @file.seek(frame[:file_position] + frame[:header_size] )
240
- # GG: Vorbis includes a preamble "vorbis", which is not included in the comments,
241
- # but is definitely in the /libvorbis/lib/info.c _vorbis_pack_comment reference implementation.
242
- # Speex does not seem to include this preamble.
243
- @file.read(7) if @bitstream_format=="vorbis"
244
- vendor_length = @file.read(4).unpack("V").first
245
- @vendor = @file.read(vendor_length)
246
-
247
- @tag = {}
248
- tag_size = @file.read(4).unpack("V")[0]
249
-
250
- tag_size.times do |i|
251
- size = @file.read(4).unpack("V")[0]
252
- comment = @file.read(size)
253
- key, val = comment.split(/=/, 2)
254
- @tag[key.downcase] = val
143
+ # Pipe input to output transforming tags along the way
144
+ # input/output must be open streams reading for reading/writing
145
+ def replace_tags(input, output, new_tags, vendor = "ruby-ogginfo")
146
+ # use the same serial number...
147
+ first_page = Page.read(input)
148
+ codec = Ogg.detect_codec(first_page)
149
+ bitstream_serial_no = first_page.bitstream_serial_no
150
+ reader = Reader.new(input)
151
+ writer = Writer.new(bitstream_serial_no, output)
152
+
153
+ # Write the first page as is (including presumably the b_o_s header)
154
+ writer.write_page(first_page)
155
+
156
+ upcased_tags = new_tags.inject({}) do |memo, (k, v)|
157
+ memo[k.upcase] = v
158
+ memo
255
159
  end
256
- end
257
-
258
- def get_length
259
- @file.seek(0)
260
- last_frame = nil
261
- begin
262
- while f = OggInfo.read_frame(@file)
263
- last_frame = f
160
+ # The codecs we know about put comments etc in following pages
161
+ # as suggested by the spec
162
+ written_pages_count = codec.replace_tags(reader, writer, upcased_tags, vendor)
163
+ if written_pages_count > 1
164
+ # Write the rest of the pages. We have to do page at a time
165
+ # because our tag replacement may have changed the number of
166
+ # pages and thus every subsequent page needs to have its
167
+ # sequence_no updated.
168
+ reader.each_pages(:skip_checksum => true) do |page|
169
+ writer.write_page(page)
264
170
  end
265
- rescue OggInfoError
171
+ else
172
+ FileUtils.copy_stream(reader.input, writer.output)
266
173
  end
267
- last_frame[:granule_pos].to_f / @samplerate
268
174
  end
269
175
 
270
176
  def convert_tag_charset(from_charset, to_charset)
271
177
  return if from_charset == to_charset
272
178
  Iconv.open(to_charset, from_charset) do |ic|
273
- @tag.each do |k, v|
274
- @tag[k] = ic.iconv(v)
179
+ tag.each do |k, v|
180
+ tag[k] = ic.iconv(v)
275
181
  end
276
182
  end
277
183
  end
@@ -101,7 +101,7 @@ EOF
101
101
 
102
102
  class OggInfoTest < Test::Unit::TestCase
103
103
 
104
- TEMP_FILE = File.join(Dir.tmpdir, "test_mp3info.ogg")
104
+ TEMP_FILE = File.join(Dir.tmpdir, "test_ogginfo.ogg")
105
105
 
106
106
  def setup
107
107
  valid_ogg_file = VALID_OGG.unpack("m*").first
@@ -123,40 +123,101 @@ class OggInfoTest < Test::Unit::TestCase
123
123
  end
124
124
 
125
125
  def test_length
126
- generate_ogg
127
- OggInfo.open("test.ogg") do |ogg|
128
- assert_in_delta(17.0, ogg.length, 1)
129
- assert_in_delta(67000.0, ogg.bitrate, 5000)
126
+ tf = generate_ogg
127
+ OggInfo.open(tf.path) do |ogg|
128
+ assert_in_delta(17.0, ogg.length, 1, "length has not been correctly guessed")
129
+ assert_in_delta(67000.0, ogg.bitrate, 2000, "bitrate has not been correctly guessed")
130
130
  end
131
131
  end
132
132
 
133
133
  def test_tag_writing
134
- generate_ogg
135
- tag = {"title" => "mytitle", "artist" => "myartist" }
136
- OggInfo.open("test.ogg") do |ogg|
137
- tag.each { |k,v| ogg.tag[k] = v }
138
- end
134
+ tag_test("title" => generate_random_string, "artist" => generate_random_string )
135
+ end
139
136
 
140
- OggInfo.open("test.ogg") do |ogg|
141
- assert_equal tag, ogg.tag
142
- end
137
+ def test_big_tags
138
+ tag_test("title" => generate_random_string(60000), "artist" => generate_random_string(60000) )
143
139
  end
140
+
144
141
 
145
142
  def test_charset
146
- generate_ogg
147
- OggInfo.open("test.ogg", "utf-8") do |ogg|
143
+ tf = generate_ogg
144
+ OggInfo.open(tf.path, "utf-8") do |ogg|
148
145
  ogg.tag["title"] = "hello\303\251"
149
146
  end
150
147
 
151
- OggInfo.open("test.ogg", "iso-8859-1") do |ogg|
148
+ OggInfo.open(tf.path, "iso-8859-1") do |ogg|
152
149
  assert_equal "hello\xe9", ogg.tag["title"]
153
150
  end
154
151
  end
155
152
 
153
+ def test_should_not_fail_when_input_is_truncated
154
+ valid_ogg = generate_ogg
155
+ ogg_length = nil
156
+ OggInfo.open(valid_ogg.path) do |ogg|
157
+ ogg_length = ogg.length
158
+ end
159
+
160
+ tf = generate_truncated_ogg
161
+ OggInfo.open(tf.path) do |truncated_ogg|
162
+ assert ogg_length != truncated_ogg.length
163
+ end
164
+
165
+ reader = Ogg::Reader.new(open(tf.path, "r"))
166
+ last_page = nil
167
+ reader.each_pages do |page|
168
+ last_page = page
169
+ end
170
+ assert_not_equal Ogg.compute_checksum(last_page.pack), last_page.checksum
171
+ end
172
+
173
+ def test_checksum
174
+ tf = generate_truncated_ogg
175
+ reader = Ogg::Reader.new(open(tf.path))
176
+ assert_raises(Ogg::StreamError) do
177
+ reader.each_pages(:checksum => true) do |page|
178
+ page
179
+ end
180
+ end
181
+ end
182
+
183
+ protected
184
+
185
+
156
186
  def generate_ogg
157
- unless test(?f, "test.ogg")
158
- system("dd if=/dev/urandom bs=1024 count=3000 | oggenc -q0 --raw -o test.ogg -") or
159
- flunk("cannot generate \"test.ogg\", tests cannot be fully performed")
187
+ generated_ogg_file_path = File.join(File.dirname(__FILE__), "test.ogg")
188
+ unless test(?f, generated_ogg_file_path)
189
+ system("dd if=/dev/urandom bs=1024 count=3000 | oggenc -q0 --raw -o #{generated_ogg_file_path} -") or
190
+ flunk("cannot generate \"#{generated_ogg_file_path}\", tests cannot be fully performed")
191
+ end
192
+ tf = Tempfile.new("ruby-ogginfo")
193
+ tf.close
194
+ FileUtils.cp(generated_ogg_file_path, tf.path)
195
+ tf
196
+ end
197
+
198
+ def generate_random_string(size = 256)
199
+ File.read("/dev/urandom", size)
200
+ end
201
+
202
+ def generate_truncated_ogg
203
+ valid_ogg = generate_ogg
204
+ tf = Tempfile.new("ruby-ogginfo")
205
+ data = File.read(valid_ogg.path, File.size(valid_ogg.path) - 10000)
206
+ tf.write(data)
207
+ tf.close
208
+ tf
209
+ end
210
+
211
+ def tag_test(tag)
212
+ tf = generate_ogg
213
+
214
+ OggInfo.open(tf.path) do |ogg|
215
+ tag.each { |k,v| ogg.tag[k] = v }
216
+ end
217
+
218
+ OggInfo.open(tf.path) do |ogg|
219
+ assert_equal tag, ogg.tag
160
220
  end
221
+ test_length
161
222
  end
162
223
  end
@@ -58,6 +58,7 @@ EOF
58
58
  class SpxInfoTest < Test::Unit::TestCase
59
59
 
60
60
  TEMP_FILE = File.join(Dir.tmpdir, "test.spxinfo.spx")
61
+ GEN_FILE = File.join(Dir.tmpdir,"test.spxgen.spx")
61
62
 
62
63
  def setup
63
64
  valid_spx_file = VALID_SPX.unpack("m*").first
@@ -73,6 +74,7 @@ class SpxInfoTest < Test::Unit::TestCase
73
74
  assert_equal 1, spx.channels
74
75
  assert_equal 32000, spx.samplerate
75
76
  assert_in_delta(0.5, spx.length, 1)
77
+ assert_equal "cityrail",spx.tag["author"]
76
78
  end
77
79
  end
78
80
 
@@ -81,20 +83,36 @@ class SpxInfoTest < Test::Unit::TestCase
81
83
  OggInfo.open("test.spx") do |spx|
82
84
  assert_equal 2, spx.channels
83
85
  assert_equal 44100, spx.samplerate
86
+ assert_equal "spxinfotest", spx.tag.author
84
87
  end
85
88
  end
86
89
 
87
90
 
88
91
  def test_charset
89
92
  generate_spx
90
- OggInfo.open("test.spx", "utf-8") do |spx|
91
- assert_equal "spxinfotest", spx.tag["author"]
93
+ FileUtils.cp("test.spx",GEN_FILE)
94
+ OggInfo.open(GEN_FILE, "utf-8") do |spx|
95
+ assert_equal "hello\303\251",spx.tag["test"]
92
96
  end
93
97
 
94
- OggInfo.open("test.spx", "iso-8859-1") do |spx|
98
+ OggInfo.open(GEN_FILE, "iso-8859-1") do |spx|
95
99
  assert_equal "hello\xe9", spx.tag["test"]
96
100
  end
97
101
  end
102
+
103
+ def test_tag_writing
104
+ generate_spx
105
+ FileUtils.cp("test.spx",GEN_FILE)
106
+ tag = {"title" => "mytitle", "test" => "myartist" }
107
+ OggInfo.open(GEN_FILE) do |spx|
108
+ spx.tag.clear
109
+ tag.each { |k,v| spx.tag[k] = v }
110
+ end
111
+
112
+ OggInfo.open(GEN_FILE) do |spx|
113
+ assert_equal tag, spx.tag
114
+ end
115
+ end
98
116
 
99
117
  def generate_spx
100
118
  unless test(?f, "test.spx")
metadata CHANGED
@@ -4,16 +4,18 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
+ - 6
7
8
  - 5
8
- version: "0.5"
9
+ version: 0.6.5
9
10
  platform: ruby
10
11
  authors:
11
12
  - Guillaume Pierronnet
13
+ - Grant Gardner
12
14
  autorequire:
13
15
  bindir: bin
14
16
  cert_chain: []
15
17
 
16
- date: 2011-01-13 00:00:00 +01:00
18
+ date: 2011-04-07 00:00:00 +02:00
17
19
  default_executable:
18
20
  dependencies:
19
21
  - !ruby/object:Gem::Dependency
@@ -48,7 +50,9 @@ description: |-
48
50
  ruby-ogginfo gives you access to low level information on ogg files
49
51
  (bitrate, length, samplerate, encoder, etc... ), as well as tag.
50
52
  It is written in pure ruby.
51
- email: moumar@rubyforge.org
53
+ email:
54
+ - moumar@rubyforge.org
55
+ - grant@lastweekend.com.au
52
56
  executables: []
53
57
 
54
58
  extensions: []
@@ -61,13 +65,19 @@ files:
61
65
  - Manifest.txt
62
66
  - README.rdoc
63
67
  - Rakefile
64
- - setup.rb
68
+ - lib/ogg/codecs/comments.rb
69
+ - lib/ogg/codecs/speex.rb
70
+ - lib/ogg/codecs/vorbis.rb
71
+ - lib/ogg/page.rb
72
+ - lib/ogg/reader.rb
73
+ - lib/ogg/writer.rb
74
+ - lib/ogg.rb
65
75
  - lib/ogginfo.rb
76
+ - setup.rb
66
77
  - test/test_ruby-ogginfo.rb
78
+ - test/test_ruby-spxinfo.rb
67
79
  has_rdoc: yard
68
- homepage: |
69
- http://ruby-ogginfo.rubyforge.org/
70
-
80
+ homepage: http://ruby-ogginfo.rubyforge.org/
71
81
  licenses: []
72
82
 
73
83
  post_install_message:
@@ -97,7 +107,7 @@ rubyforge_project: ruby-ogginfo
97
107
  rubygems_version: 1.3.6
98
108
  signing_key:
99
109
  specification_version: 3
100
- summary: ruby-ogginfo is a pure-ruby library that gives low level informations on ogg files
110
+ summary: ruby-ogginfo gives you access to low level information on ogg files (bitrate, length, samplerate, encoder, etc..
101
111
  test_files:
102
- - test/test_ruby-ogginfo.rb
103
112
  - test/test_ruby-spxinfo.rb
113
+ - test/test_ruby-ogginfo.rb