gstreamer/gst/extend/jukebox.py
Edward Hervey cef8a55b55 gst/extend/: Fixes for thread-safety, changes in behaviour with gst.Pad and cleanup. Still has some issues.
Original commit message from CVS:
* gst/extend/jukebox.py:
* gst/extend/sources.py:
Fixes for thread-safety, changes in behaviour with gst.Pad and
cleanup. Still has some issues.
2007-06-16 12:08:45 +00:00

356 lines
11 KiB
Python

#!/usr/bin/env python
# -*- Mode: Python -*-
# vi:si:et:sw=4:sts=4:ts=4
#
# GStreamer python bindings
# Copyright (C) 2005 Edward Hervey <edward at fluendo dot com>
# Copyright (C) 2005 Thomas Vander Stichele <thomas at apestaart dot org>
# 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)