# HG changeset patch # User David McClosky # Date 2010-06-22 04:41:11 # Node ID 94039df5cdd9fdbcf653eb3b6a02b8dcfd2c2287 # Parent f85304fdc9756249fcb80d3732c865c651cf9a13 create Playlist class which is now used in wavecurve, musicPad, and ascoltami2. Ignore-this: da54d0239906e81ad5d95741d22f5975 Several other refactorings. Also, totally untested. diff --git a/bin/ascoltami2 b/bin/ascoltami2 --- a/bin/ascoltami2 +++ b/bin/ascoltami2 @@ -3,33 +3,56 @@ import web, thread, gobject, sys, optpar from rdflib import URIRef sys.path.append(".") from light9.ascoltami.player import Player +from light9.ascoltami.playlist import Playlist, NoSuchSong from light9.ascoltami.webapp import makeApp from light9 import networking, showconfig -logging.basicConfig() -log = logging.getLogger() -gobject.threads_init() +class App: + def __init__(self, graph, show): + self.graph = graph + self.player = Player(onEOS=self.onEOS) + self.show = show + self.playlist = Playlist.fromShow(graph, show) -parser = optparse.OptionParser() -parser.add_option('--show', - help='show URI, like http://light9.bigasterisk.com/show/dance2008') -parser.add_option("-v", "--verbose", action="store_true", - help="logging.DEBUG") -(options, args) = parser.parse_args() + def run(self, musicPort): + # the cherrypy server would wedge when vidref pounds on it; this + # one seems to run + thread.start_new(web.httpserver.runbasic, + (makeWebApp(self).wsgifunc(), + ('0.0.0.0', musicPort))) -log.setLevel(logging.DEBUG if options.verbose else logging.INFO) + mainloop = gobject.MainLoop() + mainloop.run() + + def onEOS(self, song): + self.player.pause() + self.player.seek(0) -if not options.show: - raise ValueError("missing --show http://...") - -graph = showconfig.getGraph() -player = Player() + try: + nextSong = self.playlist.nextSong(song) + except NoSuchSong: # we're at the end of the playlist + return + + self.player.setSong(nextSong, play=False) + +if __name__ == "__main__": + logging.basicConfig() + log = logging.getLogger() + gobject.threads_init() -# the cherrypy server would wedge when vidref pounds on it; this -# one seems to run -thread.start_new(web.httpserver.runbasic, - (makeApp(player, graph, URIRef(options.show)).wsgifunc(), - ('0.0.0.0', networking.musicPort()))) + parser = optparse.OptionParser() + parser.add_option('--show', + help='show URI, like http://light9.bigasterisk.com/show/dance2008') + parser.add_option("-v", "--verbose", action="store_true", + help="logging.DEBUG") + (options, args) = parser.parse_args() -mainloop = gobject.MainLoop() -mainloop.run() + log.setLevel(logging.DEBUG if options.verbose else logging.INFO) + + if not options.show: + raise ValueError("missing --show http://...") + + graph = showconfig.getGraph() + app = App(graph, URIRef(options.show)) + musicPort = networking.musicPort() + app.run(musicPort) diff --git a/bin/musicPad b/bin/musicPad --- a/bin/musicPad +++ b/bin/musicPad @@ -6,20 +6,18 @@ import sys, wave, logging, os sys.path.append(".") from light9 import networking, showconfig from light9.namespaces import L9 +from light9.ascoltami.playlist import Playlist logging.basicConfig(level=logging.INFO) log = logging.getLogger() introPad = 4 postPad = 9 # 5 + autostop + 4 -graph = showconfig.getGraph() +playlist = Playlist.fromShow(showconfig.getGraph(), showconfig.showUri()) # instead of taking a show uri like it should, i just convert every # path i find in the graph (hoping that you only loaded statements for # the current show) -for p in sorted(graph.objects(None, L9['showPath'])): - assert p.startswith("file://") - p = p[len("file://"):] - +for p in playlist.allSongPaths(): log.info("read %s", p) inputWave = wave.open(p, 'r') diff --git a/bin/wavecurve b/bin/wavecurve --- a/bin/wavecurve +++ b/bin/wavecurve @@ -3,7 +3,6 @@ import os, sys, optparse import run_local from light9.wavepoints import simp - def createCurve(inpath, outpath, t): print "reading %s, writing %s" % (inpath, outpath) points = simp(inpath.replace('.ogg', '.wav'), seconds_per_average=t) @@ -11,33 +10,31 @@ def createCurve(inpath, outpath, t): f = file(outpath, 'w') for time_val in points: print >>f, "%s %s" % time_val - - parser = optparse.OptionParser(usage="""%prog inputSong.wav outputCurve You probably just want -a """) -parser.add_option("-t",type="float",default=.01, +parser.add_option("-t", type="float", default=.01, help="seconds per sample (default .01, .07 is smooth)") -parser.add_option("-a", action="store_true", +parser.add_option("-a", "--all", action="store_true", help="make standard curves for all songs") options,args = parser.parse_args() - -if options.a: +if options.all: from light9 import showconfig from light9.namespaces import L9 from rdflib import RDF graph = showconfig.getGraph() - for song in graph.subjects(RDF.type, L9['Song']): - inpath = showconfig.songOnDisk(song) + + playlist = Playlist.fromShow(showconfig.getGraph(), showconfig.showUri()) + for inpath in playlist.allSongPaths(): for curveName, t in [('music', .01), ('smooth_music', .07)]: outpath = showconfig.curvesDir() + "/%s-%s" % ( showconfig.songFilenameFromURI(song), curveName) createCurve(inpath, outpath, t) else: - inpath,outpath = args + inpath, outpath = args createCurve(inpath, outpath, options.t) diff --git a/light9/ascoltami/player.py b/light9/ascoltami/player.py --- a/light9/ascoltami/player.py +++ b/light9/ascoltami/player.py @@ -9,7 +9,13 @@ import gst, gobject log = logging.getLogger() class Player(object): - def __init__(self, autoStopOffset=4): + def __init__(self, autoStopOffset=4, onEOS=None): + """autoStopOffset is the number of seconds before the end of + song before automatically stopping (which is really pausing). + onEOS is an optional function to be called when we reach the + end of a stream (for example, can be used to advance the song). + It is called with one argument which is the URI of the song that + just finished.""" self.autoStopOffset = autoStopOffset self.playbin = self.pipeline = gst.parse_launch("playbin2") self.playStartTime = 0 @@ -45,12 +51,13 @@ class Player(object): if self.lastWatchTime < self.autoStopTime < t: log.info("autostop") self.pause() - if self.isPlaying() and t >= self.duration() - .2: - # i don't expect to hit dur exactly with this - # polling. What would be better would be to watch for - # the EOS signal and react to that - self.pause() - self.seek(0) + if not self.onEOS: + if self.isPlaying() and t >= self.duration() - .2: + # i don't expect to hit dur exactly with this + # polling. What would be better would be to watch for + # the EOS signal and react to that + self.onEOS(self.getSong()) + self.lastWatchTime = t except: traceback.print_exc() @@ -63,7 +70,7 @@ class Player(object): t * gst.SECOND) self.playStartTime = time.time() - def setSong(self, songUri): + def setSong(self, songUri, play=True): """ uri like file:///my/proj/light9/show/dance2010/music/07.wav """ @@ -72,8 +79,13 @@ class Player(object): self.preload(songUri) self.pipeline.set_property("uri", songUri) # todo: don't have any error report yet if the uri can't be read - self.pipeline.set_state(gst.STATE_PLAYING) - self.playStartTime = time.time() + if play: + self.pipeline.set_state(gst.STATE_PLAYING) + self.playStartTime = time.time() + + def getSong(self): + """Returns the URI of the current song.""" + return self.playbin.get_property("uri") def preload(self, songUri): """ @@ -122,6 +134,3 @@ class Player(object): def isPlaying(self): _, state, _ = self.pipeline.get_state() return state == gst.STATE_PLAYING - - - diff --git a/light9/ascoltami/playlist.py b/light9/ascoltami/playlist.py new file mode 100644 --- /dev/null +++ b/light9/ascoltami/playlist.py @@ -0,0 +1,45 @@ +from light9.showconfig import getSongsFromShow +from light9.namespaces import L9 + +class NoSuchSong(ValueError): + """Raised when a song is requested that doesn't exist (e.g. one + after the last song in the playlist).""" + +class Playlist(object): + def __init__(self, graph, playlistUri): + self.songs = list(graph.items(playlistUri)) + def nextSong(self, currentSong): + """Returns the next song in the playlist or raises NoSuchSong if + we are at the end of the playlist.""" + try: + currentIndex = self.songs.index(currentSong) + except IndexError: + raise ValueError("%r is not in the current playlist (%r)." % \ + (currentSong, self.playlistUri)) + + try: + nextSong = self.songs[currentIndex + 1] + except IndexError: + raise NoSuchSong("%r is the last item in the playlist." % \ + currentSong) + + return nextSong + def allSongs(self): + """Returns a list of all song URIs in order.""" + return self.songs + def allSongPaths(self): + """Returns a list of the filesystem paths to all songs in order.""" + paths = [] + for song in self.songs: + p = graph.value(song, L9['showPath']) + assert p.startswith("file://"), p + p = p[len("file://"):] + paths.append(p) + return paths + + @classmethod + def fromShow(playlistClass, graph, show): + playlistUri = graph.value(show, L9['playList']) + if not playlistUri: + raise ValueError("%r has no l9:playList" % show) + return playlistClass(graph, playlistUri) diff --git a/light9/ascoltami/webapp.py b/light9/ascoltami/webapp.py --- a/light9/ascoltami/webapp.py +++ b/light9/ascoltami/webapp.py @@ -1,11 +1,10 @@ import web, jsonlib from twisted.python.util import sibpath from light9.namespaces import L9 +from light9.showconfig import getSongsFromShow from rdflib import URIRef -player = None -graph = None -show = None +app = None def songLocation(graph, songUri): loc = graph.value(songUri, L9['showPath']) @@ -28,7 +27,10 @@ class root(object): class timeResource(object): def GET(self): - playingLocation = player.playbin.get_property("uri") + player = app.player + graph = app.graph + + playingLocation = player.getSong() if playingLocation: song = songUri(graph, URIRef(playingLocation)) else: @@ -47,6 +49,7 @@ class timeResource(object): with a pause/resume command too. """ params = jsonlib.read(web.data(), use_float=True) + player = app.player if params.get('pause', False): player.pause() if params.get('resume', False): @@ -57,12 +60,10 @@ class timeResource(object): class songs(object): def GET(self): - playList = graph.value(show, L9['playList']) - if not playList: - raise ValueError("%r has no l9:playList" % show) - songs = list(graph.items(playList)) + graph = app.graph - + songs = getSongsFromShow(graph, app.show) + web.header("Content-type", "application/json") return jsonlib.write({"songs" : [ {"uri" : s, @@ -72,11 +73,15 @@ class songs(object): class songResource(object): def POST(self): """post a uri of song to switch to (and start playing)""" - player.setSong(songLocation(graph, URIRef(web.data()))) + graph = app.graph + + app.player.setSong(songLocation(graph, URIRef(web.data()))) return "ok" class seekPlayOrPause(object): def POST(self): + player = app.player + data = jsonlib.read(web.data(), use_float=True) if player.isPlaying(): player.pause() @@ -84,16 +89,15 @@ class seekPlayOrPause(object): player.seek(data['t']) player.resume() -def makeApp(thePlayer, theGraph, theShow): - global player, graph, show - player, graph, show = thePlayer, theGraph, theShow +def makeWebApp(theApp): + global app + app = theApp - urls = ("/", "root", - "/time", "timeResource", - "/song", "songResource", - "/songs", "songs", - "/seekPlayOrPause", "seekPlayOrPause", + urls = (r"/", "root", + r"/time", "timeResource", + r"/song", "songResource", + r"/songs", "songs", + r"/seekPlayOrPause", "seekPlayOrPause", ) - app = web.application(urls, globals(), autoreload=False) - return app + return web.application(urls, globals(), autoreload=False) diff --git a/light9/showconfig.py b/light9/showconfig.py --- a/light9/showconfig.py +++ b/light9/showconfig.py @@ -41,6 +41,10 @@ def root(): "LIGHT9_SHOW env variable has not been set to the show root") return r +def showUri(): + """Return the show URI associated with $LIGHT9_SHOW.""" + return URIRef(file(path.join(root(), 'URI').read().strip())) + def findMpdHome(): """top of the music directory for the mpd on this system, including trailing slash""" @@ -59,7 +63,6 @@ def findMpdHome(): raise ValueError("can't find music_directory in any mpd config file") - def songInMpd(song): """ get the mpd path (with correct encoding) from the song URI @@ -98,6 +101,14 @@ def songFilenameFromURI(uri): assert isinstance(uri, URIRef) return uri.split('/')[-1] +def getSongsFromShow(graph, show): + playList = graph.value(show, L9['playList']) + if not playList: + raise ValueError("%r has no l9:playList" % show) + songs = list(graph.items(playList)) + + return songs + def curvesDir(): return path.join(root(),"curves") @@ -117,4 +128,3 @@ def prePostSong(): graph = getGraph() return [graph.value(MUS['preSong'], L9['showPath']), graph.value(MUS['postSong'], L9['showPath'])] -