diff --git a/bin/ascoltami b/bin/ascoltami deleted file mode 100644 --- a/bin/ascoltami +++ /dev/null @@ -1,588 +0,0 @@ -#!bin/python - -# this is a fork from semprini/ascotalmi to use mpd - -""" a separate player program from Semprini.py. name means 'listen to -me' in italian. - -features and limitations: - - xmlrpc interface for: - getting the current time in the playing song - requesting what song is playing - saying what song should play - - -setup: - -get mpd root path correct - -run mpd that's been modified to give precise times: - dash(pts/21):/my/dl/modified/mpd% src/mpd --no-daemon - -tell it to scan - mpc update - (mpc listall to check) - -run ascoltami - - -todo: - - -""" - -from __future__ import division,nested_scopes - -from optparse import OptionParser -import sys, os,math,time, jsonlib -from rdflib import URIRef -import Tkinter as tk -import logging -log = logging.getLogger() -log.setLevel(logging.INFO) - -from twisted.internet import reactor,tksupport -from twisted.internet.error import CannotListenError -from twisted.web import xmlrpc, server, resource - -import run_local -from light9 import networking, showconfig, wavelength -from light9.namespaces import L9, MUS -from light9.uihelpers import toplevelat - -sys.path.append("/home/drewp/projects/pympd_0") -sys.path.append("/my/proj/pympd_0") -from pympd import Mpd - -appstyle={'fg':'white','bg':'black'} - -def shortSongPath(song, all): - prefixlen = len(os.path.commonprefix(all)) - # include to the last os.sep- dont crop path elements in the middle - prefixlen = all[0].rfind(os.sep)+1 - - return os.path.splitext(song[prefixlen:])[0] - -class RestApi(resource.Resource): - isLeaf = True - def __init__(self, player): - resource.Resource.__init__(self) - self.player = player - def render_GET(self, request): - if request.postpath == ['position']: - return jsonlib.write({"song" : self.player.song_uri, - "t" : self.player.smoothCurrentTime(), - "started" : self.player.playStartTime}) - -class XMLRPCServe(xmlrpc.XMLRPC): - def __init__(self,player): - xmlrpc.XMLRPC.__init__(self) - self.player=player - - def xmlrpc_echo(self,x): - return x - - def xmlrpc_playfile(self,musicfilename): - self.player.play(musicfilename) - return 'ok' - def xmlrpc_stop(self): - self.player.state.set('stop') - return 'ok' - def xmlrpc_seek_to(self,t): - self.player.seek_to(t) - return 'ok' - def xmlrpc_seekplay_or_pause(self,t): - """either seek to t and play; or pause. this is the - curvecalc click-play interface""" - if self.player.state.get() == "play": - self.player.pause() - return 'paused' - else: - self.player.seek_to(t) - self.player.play() - return 'playing' - def xmlrpc_gettime(self): - """returns seconds from start of song""" - return float(self.player.smoothCurrentTime()) - def xmlrpc_songlength(self): - """song length, in seconds""" - return float(self.player.total_time.get()) - - def xmlrpc_song_uri(self): - """song URI, or None""" - return self.player.song_uri.encode('utf8') or "No song" - -class Player: - """semprini-style access to mpd. in here is where we add the - padding""" - - song_pad_time = 10 - - def __init__(self): - - self.mpd = Mpd() - args = (networking.mpdServer()+(self.mpd,)) - log.info("connecting to %r", args) - reactor.connectTCP(*args) - - self.state = tk.StringVar() - self.state.set("stop") # 'stop' 'pause' 'play' - - self.current_time = tk.DoubleVar() - self.total_time = tk.DoubleVar() - self.filename_var = tk.StringVar() - self.song_uri = None - self.playStartTime = 0 - - self.pre_post_names = showconfig.prePostSong() - - self.last_poll_time = None - self.last_autopause_time = None - - self.pollStatus() - - self.mpd_is_lying = False # mpd reports bad times in certain intervals - - def smoothCurrentTime(self): - """like self.current_time.get, but more accurate""" - - if self.last_poll_time and self.state.get() == 'play': - dt = time.time() - self.last_poll_time - else: - dt = 0 - return self.current_time.get() + dt - - def pollStatus(self): - if self.state.get() == 'stop': - self.current_time.set(-4) - - self.mpd.status().addCallback(self.pollStatus2) - - def pollStatus2(self, stat): - try: - if self.state.get() != stat.state: - self.state.set(stat.state) - - if hasattr(stat, 'time_elapsed'): - elapsed = stat.time_elapsed - songnum = stat.song - total = stat.time_total - if self.mpd_is_lying and elapsed < 3: - self.mpd_is_lying = False - - # mpd lies about elapsed, song, and total during the last - # .5sec of each song. so we coast through that part - if elapsed > total - .75 or self.mpd_is_lying: - if not self.mpd_is_lying: - self.mpd_is_lying = True - self.true_song_total = songnum, total - self.marked_time = time.time() - self.marked_val = elapsed - elapsed = self.marked_val + (time.time() - self.marked_time) - songnum, total = self.true_song_total - - t = -1 - if songnum == 1: - t = elapsed - elif songnum == 0: - t = elapsed - total - elif songnum == 2: - t = self.total_time.get() + elapsed - - self.current_time.set(t) - - self.last_poll_time = time.time() - - self.check_autopause() - finally: - reactor.callLater(.05, self.pollStatus) - - def set_total_time(self, song): - # currently only good for .wav - p = showconfig.songOnDisk(song) - self.total_time.set(wavelength.wavelength(p)) - - def mpdPlay(self): - """vidref wants to know when this play started""" - self.playStartTime = time.time() - self.mpd.play() - - def play(self, song=None): - if song is None: - self.mpdPlay() - return - - self.mpd.clear() - self.mpd.add(showconfig.songInMpd(MUS['preSong'])) - self.mpd.add(showconfig.songInMpd(song)) - self.mpd.add(showconfig.songInMpd(MUS['postSong'])) - - self.fillCache(song) - - self.filename_var.set(graph.value(song, L9['showPath'])) - self.song_uri = song - - self.set_total_time(song) - self.seek_to(-4) - - def fillCache(self, song): - """read the song's entire wav file into memory just before - playing, so that mpd should never hit the disk during - playback. On score in 2007, we had some mpd stutters that were - always timed with a disk read.""" - p = showconfig.songOnDisk(song) - print "reading %s to prime the cache" % p - open(p).read() - - def check_autopause(self): - pause_time = self.total_time.get() + self.song_pad_time - t = self.current_time.get() - if (self.state.get() == "play" and - self.last_autopause_time < pause_time < t): - self.mpd.pause() - self.last_autopause_time = t - - def stop(self): - self.mpd.seek(seconds=0, song=0) - self.mpd.stop() - - def seek_to(self, t): - if t < 0: - # seeking to anything within my 4-sec silence ogg goes - # right to zero. maybe ogg seeking is too coarse? - self.mpd.seek(seconds=t - (-4), song=0) - elif t > self.total_time.get(): - self.mpd.seek(seconds=t - self.total_time.get(), song=2) - else: - self.mpd.seek(seconds=t, song=1) - self.last_autopause_time = t - self.playStartTime = time.time() - - def skip_intro(self): - self.seek_to(0) - - def in_post(self): - return (self.current_time.get() > self.total_time.get() + - self.song_pad_time) - - def play_pause_toggle(self): - def finish(status): - if status.state == 'play': - self.mpd.pause() - else: - self.mpdPlay() - self.mpd.status().addCallback(finish) - - def pause(self): - self.mpd.pause() - - def skip_to_post(self): - self.seek_to(self.total_time.get() + self.song_pad_time) - self.play() - - -class GoButton: - def __init__(self, player, statusLabel, songURIs): - self.player = player - self.statusLabel = statusLabel - self.songURIs = songURIs - - self.player.current_time.trace("w", self.updateStatus) - - def _nextAction(self): - state = self.player.state.get() - if state == 'stop': - currentPath = self.player.song_uri - try: - nextPath = self.songURIs[self.songURIs.index(currentPath) + 1] - except (ValueError, IndexError): - nextPath = self.songURIs[0] - - return ("next song %s" % shortSongPath(nextPath, self.songURIs), - lambda: self.player.play(nextPath)) - - if state == 'pause': - return "play", self.player.play - - if state == 'play': - if self.player.current_time.get() < 0: - return "skip intro", self.player.skip_intro - if self.player.in_post(): - return "", lambda: None - return "skip to post", self.player.skip_to_post - - def action(self): - desc, func = self._nextAction() - func() - - def updateStatus(self, *args): - desc, func = self._nextAction() - self.statusLabel.config(text=desc) - - - -def buildsonglist(root, graph, songs, player): - songlist=tk.Frame(root,bd=2,relief='raised',bg='black') - - maxsfwidth=max([len(graph.label(song)) for song in songs]) - - for i,song in enumerate(songs): - b=tk.Button(songlist,text=graph.label(song),width=maxsfwidth, - anchor='w',pady=0,bd=0,relief='flat', - font="arial 14 bold") - b.bind("",lambda ev,b=b: - b.config(font="arial %d bold" % min(12,int((ev.height-3)*.8)))) - try: - # rainbow colors - frac=i/len(songs) - b.config(bg='black', - fg="#%02x%02x%02x" % tuple([int(255*(.7+.3* - math.sin(frac*4+x)) - ) for x in 1,2,3])) - except Exception,e: - print "rainbow failed: %s"%e - - b.config(command=lambda song=song: player.play(song)) - b.pack(side='top',fill='both',exp=1,padx=0,pady=0,ipadx=0,ipady=0) - - - def color_buttons(x, y, z, song=song, b=b): - name = player.filename_var.get() - if name == graph.value(song, L9['showPath']): - b['bg'] = 'grey50' - else: - b['bg'] = 'black' - player.filename_var.trace("w", color_buttons) - return songlist - - -class TimeScale(tk.Scale): - def __init__(self,master,player): - tk.Scale.__init__(self, master, orient="horiz", - from_=-4,to_=100, - sliderlen=20,width=20, - res=0.001, - showvalue=0, - troughcolor='black', - bg='lightblue3', - ) - - self.player = player - - self.dragging = False - self.button_down = False - self.drag_start_player_state = None - - self.bind("", self.b1down) - self.config(command=self.scale_changed) - self.bind("", self.b1up) - - self.player.current_time.trace('w', self.current_time_changed) - - def current_time_changed(self, *args): - """attach player.current_time to scale (one-way)""" - if not self.dragging: - self.set(self.player.current_time.get()) - - def b1down(self,evt): - self.button_down = True - self.dragging = False - - def scale_changed(self, time): - if not self.button_down: - return - - if not self.dragging: - self.dragging = True - self.drag_start_player_state = self.player.state.get() - if self.drag_start_player_state == "play": - # due to mpd having to seemingly play a whole block at - # every new seek point, it is better to pause playback - # while the mouse is down - self.player.pause() - - # ok to seek around when paused. this keeps the displayed time - # up to date, which is how the user knows where he is - self.player.seek_to(float(time)) - - def b1up(self,evt): - self.button_down = False - self.dragging = False - - if self.drag_start_player_state == "play": - self.player.play() - -class Seeker(tk.Frame): - """includes scale AND labels below it""" - def __init__(self,master,player): - tk.Frame.__init__(self,master,bg='black') - - self.scl = TimeScale(self,player) - self.scl.pack(fill='x',side='top') - - self.buildstatus(player) - - - def buildstatus(self,player): - left_var=tk.DoubleVar() - for txt,var,fmt in (('Current',player.current_time,"%.2f"), - ('Song len',player.total_time,"%.2f",), - ('Left',left_var, "%.2f"), - ('Song',player.filename_var, "%s"), - ('State', player.state, "%s")): - tk.Label(self,text=txt, - relief='raised',bd=1,font='arial 9', - **appstyle).pack(side='left',fill='y') - l = tk.Label(self,width=7, anchor='w', text=var.get(), - relief='ridge',bd=1,font='arial 12 bold', - padx=2,pady=2, - bg='#800000',fg='white') - l.pack(side='left',expand=1, fill='x') - - var.trace_variable('w', - lambda a,b,c,l=l,fmt=fmt,var=var: - l.config(text=fmt % var.get())) - - # update end time as the song changes - player.total_time.trace("w",lambda *args: ( - self.scl.config(to_=player.total_time.get()+15))) - - def fixleft(*args): - # update time-left variable - left_var.set(player.total_time.get()-player.current_time.get()) - - if player.current_time.get() < 0 or left_var.get() < 0: - self.scl['troughcolor'] = 'blue' - else: - self.scl['troughcolor'] = 'black' - - player.total_time.trace("w",fixleft) - player.current_time.trace("w",fixleft) - -class ControlButtons(tk.Frame): - def __init__(self, master, goButton, player, root): - tk.Frame.__init__(self,master,bg='black') - self.statebuttons = {} # lowercased name : Button - self.goButton = goButton - self.player = player - for tag, txt,cmd,key in [ - ('stop', - 'Stop\nC-s', player.stop, ""), - ('pause', - 'Pause\nC-p', player.play_pause_toggle, ""), - ('skip intro', - 'Skip Intro\nC-i', player.skip_intro, ""), - ('skip to post', - 'Skip to Post\nC-t', player.skip_to_post, ""), - ('go', 'Go\nspace', self.goButton.action, ""), - ]: - b = tk.Button(self, text=txt, command=cmd, - font='arial 16 bold', - height=3,**appstyle) - b.pack(side='left', fill='both', expand=True) - # keyboard bindings - root.bind(key, lambda evt, cmd=cmd: cmd()) - self.statebuttons[tag] = b - - def update_state_buttons(self,*args): - state = self.player.state.get() - - if state in ('stop', 'pause'): - self.statebuttons['pause']['text'] = 'Play\nC-p' - else: - self.statebuttons['pause']['text'] = 'Pause\nC-p' - - colors = {'stop' : 'red', - 'play' : 'blue', - 'pause' : 'green'} # very confusing -- play and pause supply colors - # for each other! - for name, button in self.statebuttons.items(): - if name == 'pause' and state not in ('stop', 'pause'): - name = 'play' - - if state == name: # name gets changed sometimes for 'pause' -- see right above - button['bg'] = colors.get(name, 'black') - else: - button['bg'] = 'black' - self.goButton.updateStatus() - -############################ - - - -def main(): - global graph - parser = 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") - graph = showconfig.getGraph() - (options, songfiles) = parser.parse_args() - - if options.verbose: - log.setLevel(logging.DEBUG) - - if len(songfiles)<1: - graph = showconfig.getGraph() - if not options.show: - raise ValueError("missing --show http://...") - playList = graph.value(URIRef(options.show), L9['playList']) - if not playList: - raise ValueError("%r has no l9:playList" % options.show) - songs = list(graph.items(playList)) - else: - raise NotImplementedError("don't know how to make rdf song nodes from cmdline song paths") - - - root=tk.Tk() - root.wm_title("ascoltami") - toplevelat("ascoltami", root) - root.config(bg="black") - player=Player() - - songlist = buildsonglist(root, graph, songs, player) - songlist.pack(fill='both',exp=1) - - seeker = Seeker(root, player) - - goRow = tk.Frame(root) - tk.Label(goRow, text="Go button action", - font='arial 9', **appstyle).pack(side='left', fill='both') - goStatus = tk.Label(goRow, font='arial 12 bold', **appstyle) - goStatus.config(bg='#800000', fg='white', relief='ridge') - goStatus.pack(side='left', expand=True, fill='x') - - go = GoButton(player, goStatus, songs) - - buts = ControlButtons(root, go, player, root) - - songlist.pack(fill='both', expand=True) - buts.pack(side='top', fill='x') - seeker.pack(side='top', fill='x') - goRow.pack(side='top', fill='x') - - player.state.trace_variable('w', buts.update_state_buttons) - buts.update_state_buttons() - - tksupport.install(root,ms=10) - - rootResource = resource.Resource() - site = server.Site(rootResource) - rootResource.putChild("api", RestApi(player)) - rootResource.putChild("RPC2", XMLRPCServe(player)) - - try: - reactor.listenTCP(networking.musicPort(), site) - print "started server on %s" % networking.musicPort() - except CannotListenError: - print "no server started- %s is in use" % networking.musicPort() - - root.bind("",lambda ev: reactor.stop) - root.protocol('WM_DELETE_WINDOW', reactor.stop) - - reactor.run() - -if __name__ == '__main__': - main()