ruby-ogg 0.0.1

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/lib/ogg.rb +157 -0
  2. data/lib/vorbis.rb +143 -0
  3. metadata +65 -0
data/lib/ogg.rb ADDED
@@ -0,0 +1,157 @@
1
+ require 'rubygems'
2
+ require 'bindata'
3
+
4
+ require 'stringio'
5
+
6
+ module Ogg
7
+ # This is a superclass for all custom defined Ogg decoding errors
8
+ class DecodingError < StandardError
9
+ end
10
+
11
+ # This error is raised when the file format is not valid Ogg
12
+ class MalformedFileError < DecodingError
13
+ end
14
+
15
+ # Ogg::Decoder is used to decode Ogg bitstreams. The easiest way of properly
16
+ # parsing an Ogg file is to read consecutive packets with the read_packet
17
+ # method. For example:
18
+ #
19
+ # require 'ogg'
20
+ #
21
+ # open("file.ogg", "rb") do |file|
22
+ # dec = Ogg::Decoder.new(file)
23
+ # packet = dec.read_packet
24
+ # # Do something with the packet...
25
+ # end
26
+ #
27
+ # The terms "page" and "packet" have special meanings when dealing with Ogg. A
28
+ # packet is a section of data which is encoded in the Ogg container. A page
29
+ # is a section of the Ogg container used as a means of storing packets.
30
+ # Since packets are what contain the "juicy bits" of the file, Ogg::Decoder
31
+ # provides sufficient abstraction to make handling of individual pages
32
+ # unnecessary. However, if you do need to read pages, that functionality is
33
+ # available via the read_page method.
34
+ class Decoder
35
+ # Create a new Decoder from an IO which should be open for binary reading.
36
+ def initialize io
37
+ @io = io
38
+ @packets = []
39
+ end
40
+
41
+ # Moves the file cursor forward to the next potential page. You probably
42
+ # wish to use the read_page method, which does some validation and actually
43
+ # returns the parsed page.
44
+ def seek_to_page(capture='OggS')
45
+ buffer = @io.read(capture.size)
46
+ page = nil
47
+ while not @io.eof?
48
+ if buffer == capture
49
+ @io.pos -= capture.size
50
+ return @io.pos
51
+ end
52
+ (buffer = buffer[1..-1] << @io.read(1)) rescue Exception
53
+ end
54
+
55
+ raise EOFError
56
+ end
57
+
58
+ # Seek to and read the next page from the bitstream. Returns a Page or
59
+ # nil if there are no pages left.
60
+ def read_page
61
+ page = nil
62
+ while not @io.eof?
63
+ begin
64
+ seek_to_page
65
+ page = Page.read @io
66
+ page = nil unless page.verify_checksum
67
+ break
68
+ rescue Exception => ex
69
+ # False alarm, keep looking...
70
+ end
71
+ end
72
+ return page
73
+ end
74
+
75
+ # Seek to and read the last page in the bitstream.
76
+ def read_last_page
77
+ raise 'Last page can only be read from a file stream' unless @io.is_a? File
78
+ buffer_size = 1024
79
+ pos = @io.stat.size - buffer_size
80
+ while pos > 0
81
+ @io.seek pos, IO::SEEK_SET
82
+ sio = StringIO.new @io.read(buffer_size)
83
+
84
+ dec = Decoder.new(sio)
85
+ sub_pos = nil
86
+
87
+ # Find last page in buffer
88
+ loop do
89
+ begin
90
+ sub_pos = dec.seek_to_page
91
+ sio.pos += 1
92
+ rescue
93
+ break
94
+ end
95
+ end
96
+
97
+ if sub_pos
98
+ @io.seek(pos + sub_pos, IO::SEEK_SET)
99
+ page = read_page
100
+ return page
101
+ end
102
+
103
+ pos -= buffer_size * 2 - ('OggS'.size - 1)
104
+ end
105
+
106
+ # This means that the Ogg file contains no pages
107
+ raise MalformedFileError
108
+ end
109
+
110
+ # Seek to and read the next packet in the bitstream. Returns a string
111
+ # containing the packet's binary data or nil if there are no packets
112
+ # left.
113
+ def read_packet
114
+ return @packets.pop unless @packets.empty?
115
+
116
+ while @packets.empty?
117
+ page = read_page
118
+ raise EOFError.new("End of file reached") if page.nil?
119
+ input = StringIO.new(page.data)
120
+
121
+ page.segment_table.each do |seg|
122
+ @partial ||= ""
123
+
124
+ @partial << input.read(seg)
125
+ if seg != 255
126
+ @packets.insert(0, @partial)
127
+ @partial = nil
128
+ end
129
+ end
130
+ end
131
+
132
+ return @packets.pop
133
+ end
134
+ end
135
+
136
+ # A BinData::Record which represents an Ogg page.
137
+ class Page < BinData::Record
138
+ endian :little
139
+ string :capture, :value => 'OggS', :read_length => 4
140
+ uint8 :version, :value => 0
141
+ uint8 :page_type
142
+ uint64 :granule_position
143
+ uint32 :bitstream_serial_number
144
+ uint32 :page_sequence_number
145
+ uint32 :checksum
146
+ uint8 :page_segments
147
+ array :segment_table, :type => :uint8, :initial_length => :page_segments
148
+ string :data, :read_length => lambda {segment_table.inject(0){|t,e| t+e}}
149
+
150
+ def verify_checksum
151
+ poly = 0x04c11db7
152
+ # TODO: Implement CRC
153
+ return true
154
+ end
155
+ end
156
+ end
157
+
data/lib/vorbis.rb ADDED
@@ -0,0 +1,143 @@
1
+ require 'rubygems'
2
+ require 'bindata'
3
+
4
+ module Vorbis
5
+ # This class reads metadata (such as comments/tags and bitrate) from Vorbis
6
+ # audio files. Here's an example of usage:
7
+ #
8
+ # require 'vorbis'
9
+ #
10
+ # Vorbis::Info.open('echoplex.ogg') do |info|
11
+ # info.comments[:artist].first #=> "Nine Inch Nails"
12
+ # info.comments[:title].first #=> "Echoplex"
13
+ # info.sample_rate #=> 44100
14
+ # end
15
+ #
16
+ # You may notice that for each comment field an array of values is
17
+ # available. This is because it is perfectly valid for a Vorbis file
18
+ # to have multiple artists, titles, or anything else.
19
+ class Info
20
+ attr_reader :identification_header, :comment_header
21
+ attr_reader :comments
22
+ attr_reader :duration, :sample_rate, :nominal_bitrate, :channels, :bitrate
23
+
24
+ # Create a new Vorbis::Info object for reading metadata.
25
+ def initialize(path, container=:ogg)
26
+ if path.is_a? IO
27
+ @io = path
28
+ else
29
+ @io = open(path, 'rb')
30
+ end
31
+
32
+ case container
33
+ when :ogg
34
+ init_ogg
35
+ else
36
+ raise "#{container.to_s} is not a supported container format"
37
+ end
38
+
39
+ @io.close
40
+
41
+ yield self if block_given?
42
+ end
43
+
44
+ def self.open(*args, &block)
45
+ return self.new(*args, &block)
46
+ end
47
+
48
+ # Read Vorbis metadata from within an Ogg container.
49
+ private
50
+ def init_ogg
51
+ require 'ogg'
52
+
53
+ parser = Ogg::Decoder.new @io
54
+
55
+ @identification_header = IdentificationHeader.read(parser.read_packet)
56
+ @sample_rate = @identification_header.audio_sample_rate
57
+ @nominal_bitrate = @identification_header.bitrate_nominal
58
+ @channels = @identification_header.audio_channels
59
+
60
+ @comment_header = CommentHeader.read(parser.read_packet)
61
+ @comments = @comment_header.comments
62
+
63
+ pos_after_headers = @io.pos
64
+
65
+ begin
66
+ # Duration is last granule position divided by sample rate
67
+ pos = parser.read_last_page.granule_position.to_f
68
+ @duration = pos / @sample_rate
69
+ rescue Exception
70
+ @duration = 0
71
+ end
72
+
73
+ begin
74
+ @bitrate = (file.stat.size - pos_after_headers).to_f * 8 / @duration
75
+ rescue Exception
76
+ @bitrate = 0
77
+ end
78
+ end
79
+ end
80
+
81
+ # A BinData::Record which represents a Vorbis identification header.
82
+ class IdentificationHeader < BinData::Record
83
+ endian :little
84
+ uint8 :packet_type, :value => 1
85
+ string :codec, :value => 'vorbis', :read_length => 6
86
+ uint32 :vorbis_version
87
+ uint8 :audio_channels
88
+ uint32 :audio_sample_rate
89
+ int32 :bitrate_maximum
90
+ int32 :bitrate_nominal
91
+ int32 :bitrate_minimum
92
+ bit4 :blocksize_0
93
+ bit4 :blocksize_1
94
+ end
95
+
96
+ # A simple subclass of Hash which converts keys to uppercase strings.
97
+ class InsensitiveHash < Hash
98
+ def [](key)
99
+ super(key.to_s.upcase)
100
+ end
101
+
102
+ def []=(key, value)
103
+ super(key.to_s.upcase, value)
104
+ end
105
+ end
106
+
107
+ # A BinData::BasePrimitive which represents a list of comments as per
108
+ # the Vorbis I comment header specification.
109
+ class Comments < BinData::BasePrimitive
110
+ register(self.name, self)
111
+
112
+ def read_and_return_value(io)
113
+ n_comments = read_uint32le(io)
114
+ comments = InsensitiveHash.new
115
+ n_comments.times do
116
+ length = read_uint32le(io)
117
+ comment = io.readbytes(length)
118
+ key, value = comment.split('=', 2)
119
+ (comments[key] ||= []) << value
120
+ end
121
+ return comments
122
+ end
123
+
124
+ def sensible_default
125
+ return {}
126
+ end
127
+
128
+ def read_uint32le(io)
129
+ return BinData::Uint32le.read(io)
130
+ end
131
+ end
132
+
133
+ # A BinData::Record which represents a Vorbis comment header.
134
+ class CommentHeader < BinData::Record
135
+ endian :little
136
+ uint8 :packet_type, :value => 3
137
+ string :codec, :value => 'vorbis', :read_length => 6
138
+ uint32 :vendor_length
139
+ string :vendor_string, :read_length => :vendor_length
140
+ comments :comments
141
+ end
142
+ end
143
+
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-ogg
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Aiden Nibali
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-11-24 00:00:00 +11:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bindata
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.0.0
24
+ version:
25
+ description:
26
+ email: dismal.denizen@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - lib/vorbis.rb
35
+ - lib/ogg.rb
36
+ has_rdoc: true
37
+ homepage: http://rubyforge.org/projects/ruby-ogg/
38
+ licenses: []
39
+
40
+ post_install_message:
41
+ rdoc_options: []
42
+
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: "0"
56
+ version:
57
+ requirements: []
58
+
59
+ rubyforge_project: ruby-ogg
60
+ rubygems_version: 1.3.5
61
+ signing_key:
62
+ specification_version: 3
63
+ summary: A library for reading Ogg bitstreams
64
+ test_files: []
65
+