#!/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("<Button-1>", self.button1Callback)
        self.canvas.bind("<Button-2>", self.button2Callback)
        self.canvas.bind("<Button-3>", 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();

