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
|
# 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 'forwardable'
require "tempfile"
require File.join(File.dirname(__FILE__), 'ogg.rb')
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.7.2"
extend Forwardable
include Ogg
attr_reader :channels, :samplerate, :nominal_bitrate
# +tag+ is a hash containing the vorbis tag like "Artist", "Title", and the like
attr_reader :tag
# create new instance of OggInfo
# use of charset is deprecated! please use utf-8 encoded strings and leave +charset+ to nil")
def initialize(filename, charset = nil)
if charset
warn("use of charset is deprecated! please use utf-8 encoded tags")
end
@filename = filename
@length = nil
@bitrate = nil
File.open(@filename, 'rb') do |file|
begin
info = read_headers(file)
@samplerate = info[:samplerate]
@nominal_bitrate = info[:nominal_bitrate]
@channels = info[:channels]
@tag = info[:tag]
# filesize is used to calculate bitrate
# but we don't want to include the headers
@filesize = file.stat.size - file.pos
rescue Ogg::StreamError => se
raise(OggInfoError, se.message, se.backtrace)
end
end
@original_tag = @tag.dup
end
# The length in seconds of the track
# since this requires reading the whole file we only get it
# if called
def length
unless @length
File.open(@filename) do |file|
@length = compute_length(file)
end
end
return @length
end
# Calculated bit rate, also lazily loaded
# since we depend on the length
def bitrate
@bitrate ||= (@filesize * 8).to_f / length()
end
# set a picture (.jpg or .png) on the ogg file
def picture=(filepath)
ext = File.extname(filepath)
mime_type = {
".jpg" => "image/jpeg",
".png" => "image/png"
}[ext]
description = "folder#{ext}"
raw_string =
[3, # picture type
mime_type.size,
mime_type,
description.size,
description,
0, # width
0, # height
0, # color depth
0, # number of colors used
File.size(filepath),
File.binread(filepath)
].pack("NNa*Na*NNNNNa*")
@tag["METADATA_BLOCK_PICTURE"] = [raw_string].pack("m*").strip
end
# get the picture as an array of [extension, file_content]
# or nil if not existent
def picture
extensions = {
"image/jpeg" => ".jpg",
"image/png" => ".png"
}
if content = tag["metadata_block_picture"]
_, #type, # picture type
_, # mime_type size
mime_type,
_, # description size
_, # description,
_, # width
_, # height
_, # color depth
_, # number of color used
_, #file_content_size,
file_content = content.unpack("m*").first.unpack("NNa10Na10NNNNNa*")
return [extensions[mime_type], file_content]
end
nil
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
Tempfile.open(["ruby-ogginfo", ".ogg"]) do |tempfile|
tempfile.close
tempfile = File.new(tempfile.path, "wb")
File.open(@filename, "rb") do | input |
replace_tags(input, tempfile, tag)
end
tempfile.close
FileUtils.cp(tempfile.path, @filename)
end
end
end
# check the presence of a tag
def hastag?
!tag.empty?
end
def to_s
"channels #{channels} samplerate #{samplerate} bitrate #{nominal_bitrate} #{tag.inspect}"
end
private
def read_headers(input)
reader = Reader.new(input)
codec = Ogg.detect_codec(input)
codec.decode_headers(reader)
end
# For both Vorbis and Speex, the granule_pos is the number of samples
# strictly this should be a codec function.
def compute_length(input)
reader = Reader.new(input)
last_page = nil
begin
reader.each_pages(:skip_body => true, :skip_checksum => true) do |page|
if page.granule_pos
last_page = page
end
end
rescue Ogg::StreamError
end
if last_page
return last_page.granule_pos.to_f / @samplerate
else
return 0
end
end
# Pipe input to output transforming tags along the way
# input/output must be open streams reading for reading/writing
def replace_tags(input, output, new_tags, vendor = "ruby-ogginfo")
# use the same serial number...
first_page = Page.read(input)
codec = Ogg.detect_codec(first_page)
bitstream_serial_no = first_page.bitstream_serial_no
reader = Reader.new(input)
writer = Writer.new(bitstream_serial_no, output)
# Write the first page as is (including presumably the b_o_s header)
writer.write_page(first_page)
upcased_tags = new_tags.inject({}) do |memo, (k, v)|
memo[k.upcase] = v
memo
end
# The codecs we know about put comments etc in following pages
# as suggested by the spec
written_pages_count = codec.replace_tags(reader, writer, upcased_tags, vendor)
if written_pages_count > 1
# Write the rest of the pages. We have to do page at a time
# because our tag replacement may have changed the number of
# pages and thus every subsequent page needs to have its
# sequence_no updated.
reader.each_pages(:skip_checksum => true) do |page|
writer.write_page(page)
end
else
FileUtils.copy_stream(reader.input, writer.output)
end
end
end
|