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.
- data/lib/ogg.rb +157 -0
- data/lib/vorbis.rb +143 -0
- 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
|
+
|