2006-09-12 11:14:24 +00:00
|
|
|
#!/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.
|
2007-06-16 12:08:45 +00:00
|
|
|
#
|
2006-09-12 11:14:24 +00:00
|
|
|
# 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.
|
2007-06-16 12:08:45 +00:00
|
|
|
#
|
2006-09-12 11:14:24 +00:00
|
|
|
# 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
|
2007-06-16 12:08:45 +00:00
|
|
|
gobject.threads_init()
|
2006-09-12 11:14:24 +00:00
|
|
|
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)
|
|
|
|
|
2007-06-16 12:08:45 +00:00
|
|
|
|
2006-09-12 11:14:24 +00:00
|
|
|
## 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
|
2007-06-16 12:08:45 +00:00
|
|
|
|
2006-09-12 11:14:24 +00:00
|
|
|
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)
|
2007-06-16 12:08:45 +00:00
|
|
|
|
2006-09-12 11:14:24 +00:00
|
|
|
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]
|
2007-06-16 12:08:45 +00:00
|
|
|
|
2006-09-12 11:14:24 +00:00
|
|
|
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
|
2007-06-16 12:08:45 +00:00
|
|
|
|
2006-09-12 11:14:24 +00:00
|
|
|
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)
|
2007-06-16 12:08:45 +00:00
|
|
|
self._srcpad.set_active(True)
|
2006-09-12 11:14:24 +00:00
|
|
|
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)
|
2007-06-16 12:08:45 +00:00
|
|
|
|
2006-09-12 11:14:24 +00:00
|
|
|
gobject.type_register(Jukebox)
|
2007-06-16 12:08:45 +00:00
|
|
|
|
2006-09-12 11:14:24 +00:00
|
|
|
# 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"
|
2007-06-16 12:08:45 +00:00
|
|
|
|
2006-09-12 11:14:24 +00:00
|
|
|
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:])
|
2007-06-16 12:08:45 +00:00
|
|
|
|
2006-09-12 11:14:24 +00:00
|
|
|
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)
|