#!/usr/bin/python # File: guitar-player.py # Author: Tony Cassandra # Revsion: 1.0 # Date: June 2004 # $RCSfile: guitar-player.py,v $ # $Source: /u/cvs/cassandra.org/tech-notes/guitar-player/guitar-player.py,v $ # $Revision: 1.1 $ # $Date: 2004/11/21 06:08:21 $ # This program is a wrapper around XMMS and GNUEmacs for playing MP3 # file while viewing corresponding, text-based tabulature/chord/lyric # files. # It's main assumption to function is that there are two parallel # directory structures: one for the mp3 files and one for the # tabulature/chord/lyric text files. It reads the MP3 file names and # path from the current XMMS playlist, and searches for a similarly # named file in a different root directory and with a different file # extension. Keeping these text file in sync with the playlist is the # responsibilkity of the user, and management of the playlist should # be done directly through XMMS. # Requirements/dependencies: # # o xmms # # o GNU/Emacs # # o pyxmms - installed separately # (http://people.via.ecp.fr/~flo/index.en.xhtml) # # o gnuserv/gnudoit - make sure only one is running # # o Set xmms option "No playlist advance" ########## # # 2004, Anthony R. Cassandra # # All Rights Reserved # # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose other than its incorporation into a # commercial product is hereby granted without fee, provided that the # above copyright notice appear in all copies and that both that # copyright notice and this permission notice appear in supporting # documentation. # # ANTHONY CASSANDRA DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, # INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR ANY # PARTICULAR PURPOSE. IN NO EVENT SHALL ANTHONY CASSANDRA BE LIABLE FOR # ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # ########## import os import os.path import sys import time import thread import threading import re from Tkinter import * ############################################################ # # This is the only section you should have to change to customize # for your system. # If you installed the pyxmms code in some unorthodox place, set that # place here. # xmms_lib_path = "/usr/local/lib/python2.2/site-packages" # These are the root directories locations fo rthe MP3 files, and any # text file you want to view as the song plays (tablature, chord # chart, lyrics, etc. # mp3_root = "/import/mp3" txt_root = "/import/library/music" # These are the text file suffixes that will be searched for. It is an # order list so that only the first one found is shown. # txt_suffixes = [ "tab", "crd", "btab", "txt" ] # How many seconds to pause between the time the text file is shown in # the emacs window, until the MP3 starts playing. # inter_song_pause = 5.0 play_thread_interval = 0.3 inter_xmms_cmd_delay = 0.2 num_columns=2 column_width=64 column_height=64 selected_fg="black" selected_bg="white" unselected_fg="grey16" unselected_bg="grey" # # End of customization section ############################################################ # Below here you should not have to change anything. sys.path.append( xmms_lib_path ) import xmms.common import xmms.control gDEBUG = 0 song_re = re.compile( r'^%s/(.*)\.(mp3|MP3|wav|WAV|ogg|OGG)\s*$' % mp3_root ) no_txt_file = "/tmp/._no_txt_." # Data share among threads that the lock controls. # ############################################################ # def DEBUG( msg ): if gDEBUG: print "DEBUG:",msg ############################################################ # class PlayThread(threading.Thread): """Responsible for playing songs. This includes, displaying text file, pausing and then playing.""" def __init__( this, name="PlayThread" ): threading.Thread.__init__( this, name=name ) this._play_lock = thread.allocate_lock() this._is_running = 0 this._run_lock = thread.allocate_lock() this._should_quit = 0 def run( this ): while not this._should_quit: time.sleep( play_thread_interval ) # Check to see if song just ended and advance if it has. # if this._is_running and not xmms.control.is_playing(): xmms.control.playlist_next(); this.showSong() this.playSong(); DEBUG( "run() method terminating." ) return ############################################################ # def quit( this ): """Sets the internal quit flag to stop thread execution loop.""" this._should_quit = 1 ############################################################ # def setRunning( this, value ): """Sets the internal flag to indicate that this thread should adance to the next song and play it when the current one ends.""" this._run_lock.acquire() if value: time.sleep( inter_xmms_cmd_delay ) this._is_running = 1 else: this._is_running = 0 this._run_lock.release() ############################################################ # def isRunning( this ): """Gets the internal flag to indicate that this thread should adance to the next song and play it when the current one ends.""" return this._is_running ############################################################ # def showSong( this ): """For displaying text file of the current song in the playlist. This routine is synchronized so it can be called from any thread.""" # Seems that getting a song from a playlist immediately after # changing the playlist will often give you the previous song. # Something internally that makes the issuing of commands # faster than XMMS internal can update itself. Thus, in case # we have just change the playlist position, we will always # dcelay a short time before asking it where it is at. # time.sleep( inter_xmms_cmd_delay ) DEBUG( "Acquiring lock: thread %s" % threading.currentThread()) this._play_lock.acquire() try: song_num = xmms.control.get_playlist_pos() song_path = xmms.control.get_playlist_file( song_num ) print "Song file:",song_path song_match = song_re.match( song_path ) if song_match: song_file = song_match.group( 1 ) else: print ">>> INTERNAL ERROR <<< Song name does not match." return for suffix in txt_suffixes: txt_file = "%s/%s.%s" % ( txt_root, song_file, suffix ) if os.path.isfile( txt_file ): break else: print "No text file found." txt_file = no_txt_file print "Text file:",txt_file os.system( 'gnudoit "(find-file \\"%s\\")" > /dev/null' % txt_file ) # Always want to make sure we release the lock, no matter how # we exit this method, which is why # we use python's finally clause. # finally: this._play_lock.release() DEBUG( "Releasing lock: thread %s" % threading.currentThread()) ############################################################ # def playSong( this ): """For playing a single song. This routine is synchronized so it can be called from any thread.""" DEBUG( "Acquiring lock: thread %s" % threading.currentThread()) this._play_lock.acquire() try: wait_time= int(inter_song_pause) for t in xrange(wait_time): print wait_time,"..." time.sleep( 1.0 ) wait_time -= 1 xmms.control.play( ) finally: this._play_lock.release() DEBUG( "Releasing lock: thread %s" % threading.currentThread()) ############################################################ # class GuitarPlayerGUI: def __init__( self, master ): self.commands = [ "play", "stop", "next", "next10", "step", "prev", "page-down", "prev10", "page-up", "quit" # "pause", ] self.frame = Frame(master) self.frame.pack() # Pad the command list to make it a multiple of the number of # column. This will simplify calculations. # while ( ( len(self.commands) % num_columns) != 0 ): self.commands.append( "no-op" ) self.num_columns = num_columns self.num_rows = len(self.commands) / num_columns self.window_width = num_columns * column_width self.window_height = self.num_rows * column_height self.canvas = Canvas(self.frame, width=self.window_width, height=self.window_height, bg="white" ) self.canvas.pack(side=TOP) i = 0 for cmd in self.commands: row = int( i / self.num_columns ) col = i % self.num_columns x = col * column_width y = row * column_height self.canvas.create_rectangle(x, y, x+column_width, y+column_height, fill=unselected_bg, outline="black", width=3, tags=cmd ) self.canvas.create_text( x+column_width/2, y+column_height/2, fill=unselected_fg, text=self.commands[i], tags="%s-text" % cmd ) i += 1 self.setCommandIndex( 0, 0 ) self.canvas.itemconfigure( self.commands[self.cur_cmd_idx], fill=selected_bg ) self.canvas.bind("", self.button1Callback) self.canvas.bind("", self.button2Callback) self.canvas.bind("", self.button3Callback) def setCommandIndex( self, row, col ): self.cur_cmd_row = row % self.num_rows self.cur_cmd_col = col % self.num_columns self.cur_cmd_idx = self.cur_cmd_row * num_columns + self.cur_cmd_col def button1Callback(self, event): self.doCommand( self.commands[self.cur_cmd_idx] ) def button2Callback(self, event): self.canvas.itemconfigure( self.commands[self.cur_cmd_idx], fill=unselected_bg ) self.canvas.itemconfigure( "%s-text" % self.commands[self.cur_cmd_idx], fill=unselected_fg ) self.setCommandIndex( self.cur_cmd_row+1, self.cur_cmd_col ) self.canvas.itemconfigure( self.commands[self.cur_cmd_idx], fill=selected_bg ) self.canvas.itemconfigure( "%s-text" % self.commands[self.cur_cmd_idx], fill=selected_fg ) def button3Callback(self, event): self.canvas.itemconfigure( self.commands[self.cur_cmd_idx], fill=unselected_bg ) self.canvas.itemconfigure( "%s-text" % self.commands[self.cur_cmd_idx], fill=unselected_fg ) self.setCommandIndex( self.cur_cmd_row, self.cur_cmd_col+1 ) self.canvas.itemconfigure( self.commands[self.cur_cmd_idx], fill=selected_bg ) self.canvas.itemconfigure( "%s-text" % self.commands[self.cur_cmd_idx], fill=selected_fg ) def jumpToSongNumber( self, song_num ): running_state = run_thread.isRunning() playing_state = xmms.control.is_playing() run_thread.setRunning( 0 ) xmms.control.stop(); if song_num < 0: song_num = xmms.control.get_playlist_length() - 1 if song_num > xmms.control.get_playlist_length(): song_num = 0 xmms.control.set_playlist_pos( song_num ) run_thread.showSong() if playing_state: run_thread.playSong() run_thread.setRunning( running_state ) return def doCommand( self, cmd ): if cmd == "query": print "is_paused(): ",xmms.control.is_paused(); print "is_running(): ",xmms.control.is_running(); print "is_playing(): ",xmms.control.is_playing(); return if cmd == "stop": run_thread.setRunning( 0 ) xmms.control.stop(); return if cmd == "step": run_thread.setRunning( 0 ) run_thread.playSong() return if cmd == "play": if xmms.control.is_paused(): xmms.control.play() else: run_thread.setRunning( 0 ) run_thread.playSong() run_thread.setRunning( 1 ) return if cmd == "prev": song_num = xmms.control.get_playlist_pos() - 1 self.jumpToSongNumber( song_num ) return if cmd == "next": song_num = xmms.control.get_playlist_pos() + 1 self.jumpToSongNumber( song_num ) return if cmd == "prev10": song_num = xmms.control.get_playlist_pos() - 10 self.jumpToSongNumber( song_num ) return if cmd == "next10": song_num = xmms.control.get_playlist_pos() + 10 self.jumpToSongNumber( song_num ) return if cmd == "pause": xmms.control.pause(); return if cmd == "page-down": os.system( 'gnudoit "(scroll-up)" > /dev/null' ) return if cmd == "page-up": os.system( 'gnudoit "(scroll-down)" > /dev/null' ) return if cmd == "exit" or cmd == "quit": run_thread.setRunning( 0 ) xmms.control.stop(); run_thread.quit() self.frame.quit() return if cmd == "no-op": return try: # internally 0-based, but displays 1-based song_num = int(cmd) - 1 self.jumpToSongNumber( song_num ) except ValueError: print ">>> INPUT ERROR <<< Not a number." return ############################################################ # def main( self ): run_thread.showSong() while 1: print "> ", cmd = sys.stdin.readline() cmd = cmd[0:-1] doCommand( cmd ) run_thread = PlayThread( ) run_thread.start() root = Tk() app = GuitarPlayerGUI(root) root.mainloop() # If you want to run in non-GUI mode ### OLD: app.main() run_thread.join() sys.exit( 0 ) # Functions I will probably use: # # xmms.control.play(); # xmms.control.pause(); # xmms.control.stop(); # xmms.control.is_playing(); # xmms.control.is_paused(); # xmms.control.get_playlist_pos(); # xmms.control.set_playlist_pos(pos); # xmms.control.get_playlist_length(); # xmms.control.get_output_time(); # xmms.control.jump_to_time( pos); # xxmm.control._get_playlist_file(pos); # xmms.control.playlist_prev(); # xmms.control.playlist_next(); # xmms.control.is_running(); # xmms.control.quit();