2011-03-07

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

#!/usr/bin/env 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 |busmessage|
      handle_bus_messagemessage )
    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.mergepd )
  end

  #set or get the volume
  def volume(val)
    if !val.nil? and val>=and val<=1
      @playbin.set_property("volume"val
    end
    return @playbin.get_property("volume")
  end
  
  def seek_percent(val)
    if !val.nil? and val>=and val<=1
      pd position_duration()
      duration pd['duration']
      if duration 0
        seek_loc val*duration 1000000
        seek Gst::EventSeek.new(1.0Gst::Format::Type::TIMEGst::Seek::FLAG_FLUSH.to_i Gst::Seek::FLAG_KEY_UNIT.to_iGst::Seek::TYPE_SETseek_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_messagemessage )
    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"
    $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.

Comments
2011-05-29 rekado:
re: HTTP streaming:
You may want to use "playbin2" instead of "playbin".
2012-05-18 DaKaZ:
Thanks, I found this as a great start. Switching to playbin2 does indeed get http streaming working. I also updated the class to have an equalizer (see below) for those that are interested. What is not working for me and I can't seem to really find any documentation about it is the TAG processing. GStreamer never seems to send and TAG messages, but as far as I can tell the playbin2 bin handles this by default. I wan to get Artist and Title information out of the stream. An example is: http://streampoint.radioio.com/streams/56/47bf578c13be7/ If you play this in mplayer,you'll see all the ICV tags. How do I get those in ruby-gst?

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
Name:
not required
Email:
not required (will not be displayed)
Website:
not required (will link your name to your site)
Comment:
required
Please do not post HTML code or bbcode unless you want it to show up as code in your post. (or if you are a blog spammer, in which case, you probably aren't reading this anyway).
Prove you are human by solving a math problem! I'm sorry, but due to an increase of blog spam, I've had to implement a CAPTCHA.
Problem:
1 plus 8
Answer:
required
subscribe
 
2014
2013
2012
2011
2010
December
November
October
September
August
July
June
May
April
March
February
January
2009
December
November
October
September
August
July
June
May
April
March
February
January
2008