Changeset - 5f9cf6174e62
[Not reviewed]
default
0 2 1
Drew Perttula - 18 years ago 2007-03-18 23:44:54
drewp@bigasterisk.com
ascoltami now uses rdf for config
3 files changed with 72 insertions and 35 deletions:
0 comments (0 inline, 0 general)
bin/ascoltami
Show inline comments
 
#!/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 light9.namespaces import L9, MUS
 

	
 
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"
 

	
 
    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"""
 
    
 
    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.song_uri = None
 

	
 
        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):
 
    def set_total_time(self, song):
 
        # currently only good for .wav
 
        p = os.path.join(showconfig.musicDir(), song_path)
 
        p = showconfig.songOnDisk(song)
 
        self.total_time.set(wavelength.wavelength(p))
 

	
 
    def play(self, song_path=None):
 
        if song_path is None:
 
    def play(self, song=None):
 
        if song 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.mpd.add(showconfig.songInMpd(MUS['preSong']))
 
        self.mpd.add(showconfig.songInMpd(song))
 
        self.mpd.add(showconfig.songInMpd(MUS['postSong']))
 
        self.filename_var.set(graph.value(song, L9['showPath']))
 
        self.song_uri = song
 

	
 
        self.set_total_time(song_path)
 
        self.set_total_time(song)
 
        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):
 
def buildsonglist(root, graph, songs, 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])
 
    maxsfwidth=max([len(graph.label(song)) for song in songs])
 

	
 
    for i,sf in enumerate(songfiles):
 
        b=tk.Button(songlist,text=sf[prefixlen:],width=maxsfwidth,
 
    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("<Configure>",lambda ev,b=b:
 
               b.config(font="arial %d bold" % min(12,int((ev.height-3)*.8))))
 
        try:
 
            # rainbow colors
 
            frac=i/len(songfiles)
 
            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 sf=sf: player.play(sf))
 
        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, sf=sf, b=b):
 
        def color_buttons(x, y, z, song=song, b=b):
 
            name = player.filename_var.get()
 
            if name == sf[prefixlen:]:
 
            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("<ButtonPress-1>", self.b1down)
 
        self.config(command=self.scale_changed)
 
        self.bind("<ButtonRelease-1>", 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, "<Control-s>"),
 
            ('Pause', player.play_pause_toggle, "<Control-p>"),
 
            ('Skip Intro',lambda: player.seek_to(0), "<Control-i>"),
 
            ]:
 
            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()
 

	
 
graph = showconfig.getGraph()
 

	
 
if len(songfiles)<1:
 
    songfiles = [f for f in os.listdir(showconfig.musicDir())
 
                 if f.endswith('wav')]
 
    songfiles.sort()
 
    graph = showconfig.getGraph()
 
    playList = graph.value(L9['show/dance2007'], L9['playList'])
 
    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")
 
#root.wm_geometry("+1270+440")
 
root.config(bg="black")
 
player=Player(None,None)
 

	
 
songlist = buildsonglist(root,songfiles,player)
 
songlist = buildsonglist(root, graph, songs, 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("<Destroy>",lambda ev: reactor.stop)
 
root.protocol('WM_DELETE_WINDOW', reactor.stop)
 

	
 
reactor.run()
light9/namespaces.py
Show inline comments
 
new file 100644
 
from rdflib import Namespace
 

	
 
L9 = Namespace("http://light9.bigasterisk.com/")
 
MUS = Namespace("http://light9.bigasterisk.com/music/")
light9/showconfig.py
Show inline comments
 
from os import path,getenv
 
import ConfigParser
 
from rdflib.Graph import Graph
 
from rdflib import URIRef
 
from namespaces import MUS, L9
 

	
 
def getGraph():
 
    graph = Graph()
 
    graph.parse(path.join(root(), 'config.n3'), format='n3')
 
    return graph
 

	
 
def root():
 
    r = getenv("LIGHT9_SHOW")
 
    if r is None:
 
        raise OSError(
 
            "LIGHT9_SHOW env variable has not been set to the show root")
 
    return r
 

	
 
def musicDir():
 
    return path.join(root(),"music")
 

	
 
def songInMpd(song):
 

	
 
    """mpd only works off its own musicroot, which for me is
 
    /my/music. song is a file in musicDir; this function returns a
 
    version starting with the mpd path, but minus the mpd root itself.
 
    the mpc ~/.mpdconf """
 
    
 
    if 'dance2005' in root():
 
        return "projects/dance2005/%s" % song
 
    raise NotImplementedError
 

	
 
    assert isinstance(song, URIRef), "songInMpd now takes URIRefs"
 

	
 
    mpdHome = None
 
    for line in open(path.expanduser("~/.mpdconf")):
 
        if line.startswith("music_directory"):
 
            mpdHome = line.split()[1].strip('"')
 
    if mpdHome is None:
 
        raise ValueError("can't find music_directory in your ~/.mpdconf")
 
    mpdHome = mpdHome.rstrip(path.sep) + path.sep
 

	
 
    songFullPath = songOnDisk(song)
 
    if not songFullPath.startswith(mpdHome):
 
        raise ValueError("the song path %r is not under your MPD music_directory (%r)" % (songFullPath, mpdHome))
 
        
 
    mpdRelativePath = songFullPath[len(mpdHome):]
 
    if path.join(mpdHome, mpdRelativePath) != songFullPath:
 
        raise ValueError("%r + %r doesn't make the songpath %r" % (mpdHome, mpdRelativePath, songFullPath))
 
    return mpdRelativePath.encode('ascii')
 

	
 
def songOnDisk(song):
 
    graph = getGraph()
 
    songFullPath = path.join(root(), graph.value(song, L9['showPath']))
 
    return songFullPath
 

	
 
def curvesDir():
 
    return path.join(root(),"curves")
 

	
 
def songFilename(song):
 
    return path.join(musicDir(),"%s.wav" % song)
 

	
 
def subtermsForSong(song):
 
    return path.join(root(),"subterms",song)
 

	
 
def subFile(subname):
 
    return path.join(root(),"subs",subname)
 

	
 
def subsDir():
 
    return path.join(root(),'subs')
 

	
 
def patchData():
 
    return path.join(root(),"patchdata.py")
 

	
 
def prePostSong():
 
    p = ConfigParser.SafeConfigParser()
 
    p.read([path.join(root(),'config')])
 
    return p.get('music','preSong'), p.get('music','postSong')
 
    graph = getGraph()
 
    return [graph.value(MUS['preSong'], L9['showPath']),
 
            graph.value(MUS['postSong'], L9['showPath'])]
 

	
0 comments (0 inline, 0 general)