#!/usr/bin/env 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 todo: """ from __future__ import division,nested_scopes from optparse import OptionParser import os,math,time import Tkinter as tk from twisted.internet import reactor,tksupport from twisted.internet.error import CannotListenError from twisted.web import xmlrpc, server import run_local from light9 import networking, showconfig, wavelength from pympd import Mpd appstyle={'fg':'white','bg':'black'} 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_songname(self): """song filename, or None""" return self.player.filename_var.get() or "No song" class Player: """semprini-style access to mpd. in here is where we add the padding""" def __init__(self, app, playlist, media=None): self.mpd = Mpd() reactor.connectTCP(*(networking.mpdServer()+(self.mpd,))) 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.pre_post_names = showconfig.prePostSong() self.last_poll_time = None self.pollStatus() self.autopausedthissong = False # a song only autopauses once 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_path): # currently only good for .wav p = os.path.join(showconfig.musicDir(), song_path) self.total_time.set(wavelength.wavelength(p)) def play(self, song_path=None): if song_path is None: self.mpd.play() return self.autopausedthissong = False self.mpd.clear() self.mpd.add(showconfig.songInMpd(self.pre_post_names[0])) self.mpd.add(showconfig.songInMpd(song_path)) self.mpd.add(showconfig.songInMpd(self.pre_post_names[1])) self.filename_var.set(song_path) self.set_total_time(song_path) self.mpd.seek(seconds=0, song=0) def check_autopause(self): pause_time = self.total_time.get() + 10 if (not self.autopausedthissong and self.state.get() == "play" and self.current_time.get() > pause_time): self.autopausedthissong = True self.mpd.pause() def stop(self): self.mpd.seek(seconds=0, song=0) self.mpd.stop() def seek_to(self, time): if time < 0: # seeking to anything within my 4-sec silence ogg goes # right to zero. maybe ogg seeking is too coarse? self.mpd.seek(seconds=time - (-4), song=0) elif time > self.total_time.get(): self.mpd.seek(seconds=time - self.total_time.get(), song=2) else: self.mpd.seek(seconds=time, song=1) def play_pause_toggle(self): def finish(status): if status.state == 'play': self.mpd.pause() else: self.mpd.play() self.mpd.status().addCallback(finish) def pause(self): self.mpd.pause() def buildsonglist(root,songfiles,player): songlist=tk.Frame(root,bd=2,relief='raised',bg='black') prefixlen=len(os.path.commonprefix(songfiles)) # include to the last os.sep- dont crop path elements in the middle prefixlen=songfiles[0].rfind(os.sep)+1 maxsfwidth=max([len(x[prefixlen:]) for x in songfiles]) for i,sf in enumerate(songfiles): b=tk.Button(songlist,text=sf[prefixlen:],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(songfiles) 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 sf=sf: player.play(sf)) b.pack(side='top',fill='both',exp=1,padx=0,pady=0,ipadx=0,ipady=0) def color_buttons(x, y, z, sf=sf, b=b): name = player.filename_var.get() if name == sf[prefixlen:]: 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='sunken',bd=1,font='arial 12 bold', padx=2,pady=2, bg='#800000',fg='white') if txt == 'Song': l.config(anchor='e') 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): tk.Frame.__init__(self,master,bg='black') self.statebuttons = {} # lowercased name : Button for txt,cmd,key in [ ('Stop', player.stop, ""), ('Pause', player.play_pause_toggle, ""), ('Skip Intro',lambda: player.seek_to(0), ""), ]: b = tk.Button(self, text=txt, command=cmd, font='arial 16 bold', height=3,**appstyle) b.pack(side='left', fill='x', expand=1) # keyboard bindings root.bind(key, lambda evt, cmd=cmd: cmd()) self.statebuttons[txt.lower()] = b def update_state_buttons(self,*args): state = player.state.get() if state in ('stop', 'pause'): self.statebuttons['pause']['text'] = 'Play' else: self.statebuttons['pause']['text'] = 'Pause' 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' ############################ parser=OptionParser() (options,songfiles)=parser.parse_args() if len(songfiles)<1: songfiles = [f for f in os.listdir(showconfig.musicDir()) if f.endswith('wav')] songfiles.sort() root=tk.Tk() root.wm_title("ascoltami") #root.wm_geometry("+1270+440") root.config(bg="black") player=Player(None,None) songlist = buildsonglist(root,songfiles,player) songlist.pack(fill='both',exp=1) f2 = tk.Frame(bg='black') buts = ControlButtons(f2) buts.pack(side='top',fill='x') player.state.trace_variable('w', buts.update_state_buttons) buts.update_state_buttons() seeker = Seeker(f2,player) seeker.pack(fill='x',exp=1) f2.pack(side='bottom',fill='x') tksupport.install(root,ms=10) try: reactor.listenTCP(networking.musicPort(),server.Site(XMLRPCServe(player))) 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()