The Need
For part of a Ruby project I've been working on, I needed a way to play audio files, and since I am using the project to teach myself the basics of Ruby programming, I decided to write my own audio player class utilizing the Gstreamer Multimedia framework.
Having written what is essentially the same code in both Vala and Python programming languages, I thought this would be fairly simple with the aid of the documentation.
RTFM (a rant)
The documentation for using Gstreamer with Ruby is part of the Ruby Gnome2 documentation and I found it to be dreadful to use. Since it is difficult to actually find a link to the Gstreamer related documents, I'll include a link http://ruby-gnome2.sourceforge.jp/hiki.cgi?Ruby/GStreamer
Honestly, I tried to read the documentation and it was so frustrating that I started to hate Ruby. What bothered me the most about the documentation wasn't the abundant amount of missing information, it was the 500 Server Error that I would see every 4 out of 5 clicks. Why someone thought it would be a good idea to server the files as a CGI wiki and not as good old static files is beyond me. Aaahhhhhhh! I hate that crap!
OK, time to relax and just look at some code.
Enter The Ruby
require 'thread'
require 'gst' #gem install gstreamer
#the gst namespace is Gst
#initialize gst
Gst.init
class Player
def initialize()
#create a thread for a glib main loop
thread = Thread.new() do
@mainloop = GLib::MainLoop.new
@mainloop.run
end
#make a few queries
@query_position = Gst::QueryPosition.new(Gst::Format::TIME)
@query_duration = Gst::QueryDuration.new(Gst::Format::TIME)
#make the playbin
@playbin = Gst::ElementFactory.make("playbin")
#get the playbins bus
bus = @playbin.bus
#watch the bus for messages
bus.add_watch do |bus, message|
handle_bus_message( message )
end
end
#we will need to get the current position and duration of the playbin
def position_duration()
begin
#run the query
@playbin.query( @query_position )
#what is that, picoseconds? I'll take milliseconds thank you.
position = position = @query_position.parse[1] / 1000000
@playbin.query( @query_duration )
duration = @query_duration.parse[1] / 1000000
rescue
position = 0
duration = 0
end
return {'position'=>position,'duration'=>duration}
end
def status()
#get the state
bin_state = @playbin.get_state
#isn't there a better way to convert the state to a string?
case bin_state[1]
when Gst::State::NULL
state = 'NULL'
when Gst::State::PAUSED
state = 'PAUSED'
when Gst::State::PLAYING
state = 'PLAYING'
when Gst::State::READY
state = 'READY'
end
volume = @playbin.get_property("volume")
uri = @playbin.get_property("uri")
pd = position_duration()
#return state, volume
status_hash = {'state'=>state, 'volume'=>volume, 'uri'=>uri}
#add the position and duration to the hash, and return
return status_hash.merge( pd )
end
#set or get the volume
def volume(val)
if !val.nil? and val>=0 and val<=1
@playbin.set_property("volume", val)
end
return @playbin.get_property("volume")
end
def seek_percent(val)
if !val.nil? and val>=0 and val<=1
pd = position_duration()
duration = pd['duration']
if duration > 0
seek_loc = val*duration * 1000000
seek = Gst::EventSeek.new(1.0, Gst::Format::Type::TIME, Gst::Seek::FLAG_FLUSH.to_i | Gst::Seek::FLAG_KEY_UNIT.to_i, Gst::Seek::TYPE_SET, seek_loc , Gst::Seek::TYPE_NONE, -1)
@playbin.send_event(seek)
end
end
return position_duration()
end
def quit()
@playbin.stop
@mainloop.quit
#I thought no one liked a quitter?
end
def set_uri(uri)
#null the playbin state
@playbin.set_state(Gst::State::NULL)
#set the uri
@playbin.set_property("uri",uri)
end
def play()
#really? just play
@playbin.play
end
def pause()
#really? just play
@playbin.pause
end
def handle_bus_message( message )
case message.type
when Gst::Message::Type::ERROR
#null the pipeline
@playbin.set_state(Gst::State::NULL)
#TODO: send a signal that playing is finished
when Gst::Message::Type::EOS
#null the pipeline
@playbin.set_state(Gst::State::NULL);
#TODO: send a signal that playing is finished
when Gst::Message::Type::TAG
tag_list = message.parse()
#we need to get the key and value from the tag
tag_list.each do |key,val|
#TODO: store some of this data
end
when Gst::Message::Type::STATE_CHANGED
state = @playbin.get_state
else
#what should we do?
end
#return true or shit breaks: why is this?
true
end
end
if \_\_FILE__ == $0
input = ARGV[0]
if input.match(/^http:\/\//)
#why the hell doesn't this work?
uri = input
else
uri = "file://"+File.absolute_path(ARGV[0])
end
player = Player.new
player.set_uri(uri)
player.play()
loop = true
sleep 1
while loop
puts "type 'quit' to quit"
s = $stdin.gets.chomp
if s.eql? "quit"
loop = false
end
end
player.quit()
end
For some reason the code will not play an audio file over HTTP and this bothered me for a bit, then I decided that I just don't care. One thing you may notice is that this class will create a new thread for running a GLib mainloop. Had this class been part of a larger project that uses a GLib mainloop, the new thread probably wouldn't be necessary, but hey, I'm not writing a GLib based project.
You may want to use "playbin2" instead of "playbin".
Thanks again for the start here, hope you enjoy the equalizer.
First: add this to initialize()
bin = Gst::Bin.new()
@eq = Gst::ElementFactory.make("equalizer-10bands")
autosink = Gst::ElementFactory.make("autoaudiosink")
bin.add(@eq)
bin.add(autosink)
@eq >> autosink
eqpad = @eq.get_pad("sink")
gpad = Gst::GhostPad.new("gpad", eqpad) # playbin2 requires a ghost pad, not sure why
bin.add_pad(gpad)
@playbin.audio_sink = bin
Then add this function to the class:
#set or get the equalizer, pass in a hash of one or more bands { :band0 => 10, :band1 => 5, ... :band9 => 10}
def eq(bands = {})
b = {
:band0 => @eq.band0,
:band1 => @eq.band1,
:band2 => @eq.band2,
:band3 => @eq.band3,
:band4 => @eq.band4,
:band5 => @eq.band5,
:band6 => @eq.band6,
:band7 => @eq.band7,
:band8 => @eq.band8,
:band9 => @eq.band9
}.merge(bands)
@eq.band0 = b[:band0]
@eq.band1 = b[:band1]
@eq.band2 = b[:band2]
@eq.band3 = b[:band3]
@eq.band4 = b[:band4]
@eq.band5 = b[:band5]
@eq.band6 = b[:band6]
@eq.band7 = b[:band7]
@eq.band8 = b[:band8]
@eq.band9 = b[:band9]
return b
end