#!/usr/bin/env python # -*- Mode: Python -*- # vi:si:et:sw=4:sts=4:ts=4 # # GStreamer python bindings # Copyright (C) 2005 Edward Hervey # Copyright (C) 2005 Thomas Vander Stichele # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import os import sys import pickle import random as rand import gobject gobject.threads_init() import pygst pygst.require('0.10') import gst import utils from pygobject import gsignal import sources from leveller import Leveller class Jukebox(gst.Bin): gsignal('done', str) gsignal('prerolled') # emitted when at least 2 sources are ready gsignal('changed', str, gobject.TYPE_UINT64) # clocktime, filename gsignal('looped') def __init__(self, files, rms=0.2, loops=0, random=False, caps="audio/x-raw-int,channels=2,rate=44100", picklepath='level.pck'): # with pygtk 2.4 this call is needed for the gsignal to work self.__gobject_init__() self._target_rms = rms self._loopsleft = loops self._loopsdone = 0 self._random = random self._picklepath = picklepath self._caps = gst.caps_from_string(caps) self._files = files[:] # copy self._levels = {} # filename -> rms, mixin, mixout, length self._prerolled = False self._playing = False self._scani = 0 # index into self._files for scanning self._playi = 0 # index into self._files for playing self._lastadded = None # last file added to composition self._lastposition = long(0) # last position where file was added if not len(files) > 1: raise TypeError, 'Must have at least 2 files' self._composition = gst.element_factory_make("gnlcomposition") self._composition.connect('pad-added', self._composition_pad_added_cb) self.add(self._composition) self._srcpad = None # load our pickle if it exists if os.path.exists(self._picklepath): file = open(self._picklepath) self._levels = pickle.load(file) file.close() # randomize our list if asked for if self._random: self._files = rand.sample(self._files, len(self._files)) ## public API def preroll(self): # scan the first few files and start playing gst.debug("starting jukebox prerolling") self._scan() def start(self): ## ## FIXME : THIS SHOULD'T BE NEEDED ! ## USE STATE CHANGES INSTEAD ## if not self._prerolled: raise Exception, "baby" self.set_state(gst.STATE_PAUSED) ## Scanning private methods def _scan(self): # start a leveller for a new _toscan file if self._scani >= len(self._files): gst.debug("We're done scanning !") return file = self._files[self._scani] self._scani += 1 if file in self._levels.keys(): gst.debug("already did file %s" % file) self._check_prerolled() gobject.timeout_add(0, self._scan) return gst.debug("creating leveller for %s" % file) leveller = Leveller(file) leveller.connect('done', self._leveller_done_cb, file) gobject.timeout_add(0, leveller.start) ##gobject.idle_add(leveller.iterate) def _leveller_done_cb(self, l, reason, file): if reason != sources.EOS: gst.debug("Error: %s" % reason) return gst.debug("in: %s, out: %s" % (gst.TIME_ARGS(l.mixin), gst.TIME_ARGS(l.mixout))) gst.debug("rms: %f, %f dB" % (l.rms, l.rmsdB)) # store infos self._levels[file] = (l.rms, l.mixin, l.mixout, l.length) gst.debug("writing level pickle") file = open(self._picklepath, "w") pickle.dump(self._levels, file) file.close() self._check_prerolled() self._scan() # clean up leveller after this handler gobject.timeout_add(0, l.clean) ## GnlSource-related methods def _new_gnl_source(self, location, start): """ Creates a new GnlSource containing an AudioSource with the following properties correctly set: _ volume level _ priority _ duration The start position MUST be given """ if not self._levels[location]: return None self.debug("Creating new GnlSource at %s for %s" % (gst.TIME_ARGS(start), location)) idx = self._files.index(location) + self._loopsdone * len(self._files) rms, mixin, mixout, duration = self._levels[location] gnls = gst.element_factory_make("gnlsource", "source-%d-%s" % (idx, location)) src = sources.AudioSource(location) gnls.add(src) # set volume level = 1.0 if rms > self._target_rms: level = self._target_rms / rms gst.debug('setting volume of %f' % level) else: gst.debug('not going to go above 1.0 level') src.set_volume(level) # set proper position/duration/priority in composition gnls.props.priority = (2 * self._loopsdone) + 1 + (idx % 2) gnls.props.start = long(start) gnls.props.duration = long(duration) gnls.props.media_duration = long(duration) gnls.props.media_start = long(0) return gnls def _new_mixer(self, start, duration): gnlo = gst.element_factory_make("gnloperation") ad = gst.element_factory_make("adder") gnlo.add(ad) gnlo.props.sinks = 2 gnlo.props.start = start gnlo.props.duration = duration gnlo.props.priority = 0 return gnlo def _append_file(self, location): """ Appends the given file to the composition, along with the proper mixer effect """ self.debug("location:%s" % location) start = self._lastposition if self._lastadded: start += self._levels[self._lastadded][2] start -= self._levels[location][1] gnls = self._new_gnl_source(location, start) self._composition.add(gnls) if self._lastadded: # create the mixer duration = self._levels[self._lastadded][3] - self._levels[self._lastadded][2] + self._levels[location][1] mixer = self._new_mixer(start, duration) self._composition.add(mixer) self._lastposition = start self._lastadded = location self.debug("lastposition:%s , lastadded:%s" % (gst.TIME_ARGS(self._lastposition), self._lastadded)) def _check_prerolled(self): gst.debug("_check_prerolled: index: scan %d, play %d" % ( self._scani, self._playi)) if not self._prerolled and self._scani > self._playi + 1: self._prerolled = True # add initial sources here self._append_file(self._files[0]) self._append_file(self._files[1]) self.debug("now prerolled and ready to play") self.emit('prerolled') def _emit_changed(self, file, when): print "emitting changed for %s at %r" % (file, when) self.emit('changed', file, when) def _source_clean(self, source): source.set_state(gst.STATE_NULL) self.remove(source) source.clean() ## composition callbacks def _composition_pad_added_cb(self, comp, pad): if self._srcpad: return self.debug("Ghosting source pad %s" % pad) self._srcpad = gst.GhostPad("src", pad) self._srcpad.set_active(True) self.add_pad(self._srcpad) ## gst.Bin/Element virtual methods def do_handle_message(self, message): self.debug("got message %s / %s / %r" % (message.src.get_name(), message.type.first_value_name, message)) # chaining up gst.Bin.do_handle_message(self, message) def do_state_change(self, transition): if not self._prerolled: gst.error("Call Jukebox.preroll() before!") return gst.STATE_CHANGE_FAILURE # chaining up return gst.Bin.do_state_change(self, message) gobject.type_register(Jukebox) # helper functions def _find_elements_recurse(element): if not isinstance(element, gst.Bin): return [element, ] l = [] for e in element.elements(): l.extend(_find_elements_recurse(e)) return l def _find_unconnected_pad(bin, direction): for e in _find_elements_recurse(bin): for p in e.pads(): if p.get_direction() == direction and not p.get_peer(): return p return None # run us to test if __name__ == "__main__": main = gobject.MainLoop() pipeline = gst.Pipeline('jukebox') list = open(sys.argv[1]).read().rstrip().split('\n') print list #source = Jukebox(list, random=True, loops=-1) source = Jukebox(list, random=True, loops=1) def _jukebox_prerolled_cb(jukebox): print "prerolled" _start() def _jukebox_changed_cb(jukebox, filename, when): print "changed file to %s at %s" % (filename, float(when) / gst.TIME_ARGS(gst.SECOND)) def _jukebox_looped_cb(jukebox): print "jukebox looped" def _start(): source.start() print "setting pipeline to PLAYING" pipeline.set_state(gst.STATE_PLAYING) print "set pipeline to PLAYING" def jukebox_pad_added(comp, pad, sinkpad): pad.link(sinkpad) def jukebox_message(bus, message): if message.type == gst.MESSAGE_ERROR: print "Error: %s" % message.parse_error() main.quit() elif message.type == gst.MESSAGE_EOS: print "done" main.quit() source.connect('prerolled', _jukebox_prerolled_cb) source.connect('changed', _jukebox_changed_cb) source.connect('looped', _jukebox_looped_cb) source.preroll() pipeline.add(source) bus = pipeline.get_bus() bus.add_signal_watch() bus.connect("message", jukebox_message) p = "alsasink" if len(sys.argv) > 2: p = " ".join(sys.argv[2:]) print "parsing output pipeline %s" % p sinkbin = gst.parse_launch("bin.( %s )" % p) pipeline.add(sinkbin) apad = _find_unconnected_pad(sinkbin, gst.PAD_SINK) if not apad: raise TypeError, "No unconnected sink pad found in bin %r" % sinkbin sinkpad = gst.GhostPad("sink", apad) sinkbin.add_pad(sinkpad) source.connect('pad-added', jukebox_pad_added, sinkpad) print "Going into main loop" sys.stdout.flush() main.run() print "Left main loop" sys.stdout.flush() pipeline.set_state(gst.STATE_NULL)