Wednesday, June 26, 2013

Building a python media player: Part 2

I've been working heavily on the code for Piamp, and I'm getting closer to a stable enough point where I can go through the code step by step. There are just a few features left to implement before I can get there. In the meantime, here is an updated screenshot of the player:


The most notable difference here is clearly the volume slider, but under the hood I've rewritten just about the whole thing. Here is the current piamp code:

#---------------------------------------------------------------------
#        piamp.py
#  
#      A Python Media Player
#
#      By: Josh Archer
#---------------------------------------------------------------------

import os
import pyglet
import Tkinter, tkFileDialog

dir = pyglet.resource.get_settings_path('Piamp')
if not os.path.exists(dir):
 os.makedirs(dir)
configFileName = os.path.join(dir, 'settings.cfg')

pyglet.resource.path = ['resources']
pyglet.resource.reindex()
batch = pyglet.graphics.Batch()

class Piamp(pyglet.window.Window):
 def __init__(self):
  super(Piamp, self).__init__()
  self.set_size(800, 480)
  #self.set_fullscreen(True)
  
  configFile = open(configFileName, 'wt')
  
  self.load_graphics()
  self.canPlay = True
  self.canMute = True
  self.mainMenuVisible = True
  self.frameCount = 0
  
  self.selectedFile = "null"
  
  self.mediaType = 'Audio'
 
  self.currentVolume = 0.5
  
  self.refresh_player()
  
 def load_graphics(self):
  #Background:
  self.img_background = pyglet.resource.image('background.png')
  self.img_background.anchor_x = self.img_background.width/2
  self.img_background.anchor_y = self.img_background.height/2
  
  #Main Menu Buttons:
  self.img_movies = pyglet.resource.image('movies.png')
  self.img_movies.anchor_x = self.img_movies.width/2
  self.img_movies.anchor_y = self.img_movies.height/2
  
  self.img_music = pyglet.resource.image('music.png')
  self.img_music.anchor_x = self.img_music.width/2
  self.img_music.anchor_y = self.img_music.height/2
  
  self.img_shutdown = pyglet.resource.image('shutdown.png')
  self.img_shutdown.anchor_x = self.img_shutdown.width/2
  self.img_shutdown.anchor_y = self.img_shutdown.height/2
  
  self.img_reboot = pyglet.resource.image('reboot.png')
  self.img_reboot.anchor_x = self.img_reboot.width/2
  self.img_reboot.anchor_y = self.img_reboot.height/2
  
  self.img_eq = pyglet.resource.image('equalizer.png')
  self.img_eq.anchor_x = self.img_eq.width/2
  self.img_eq.anchor_y = self.img_eq.height/2
  
  
  #Player Control Buttons:
  self.img_mute = pyglet.resource.image('mute.png')
  self.img_mute.anchor_x = self.img_mute.width/2
  self.img_mute.anchor_y = self.img_mute.height/2
  
  self.img_unmute = pyglet.resource.image('unmute.png')
  self.img_unmute.anchor_x = self.img_unmute.width/2
  self.img_unmute.anchor_y = self.img_unmute.height/2
  
  self.img_prev = pyglet.resource.image('previous.png')
  self.img_prev.anchor_x = self.img_prev.width/2
  self.img_prev.anchor_y = self.img_prev.height/2
  
  self.img_rewind = pyglet.resource.image('rewind.png')
  self.img_rewind.anchor_x = self.img_rewind.width/2
  self.img_rewind.anchor_y = self.img_rewind.height/2
  
  self.img_play = pyglet.resource.image('play.png')
  self.img_play.anchor_x = self.img_play.width/2
  self.img_play.anchor_y = self.img_play.height/2
  
  self.img_pause = pyglet.resource.image('pause.png')
  self.img_pause.anchor_x = self.img_pause.width/2
  self.img_pause.anchor_y = self.img_pause.height/2
  
  self.img_ff = pyglet.resource.image('ff.png')
  self.img_ff.anchor_x = self.img_ff.width/2
  self.img_ff.anchor_y = self.img_ff.height/2
  
  self.img_next = pyglet.resource.image('next.png')
  self.img_next.anchor_x = self.img_next.width/2
  self.img_next.anchor_y = self.img_next.height/2
  
  #Volume Control Graphics
  self.img_volume = pyglet.resource.image('volume.png')
  self.img_volume.anchor_y = self.img_volume.height/2
  
  self.img_volume_fill = pyglet.resource.image('volume_fill.png')
  #self.img_volume_fill.anchor_y = self.img_volume_fill.height/2
  
  #self.maxFill = self.img_volume_fill.width
  
 def update_sprites(self):
  #print("Entering update_sprites function...")
  
  self.backgroundSprite = pyglet.sprite.Sprite(self.img_background, x=400, y=240, batch=batch)
  
  if(self.mainMenuVisible == True):
   self.mainMenuSprites = [pyglet.sprite.Sprite(self.img_movies, x=280, y=315, batch=batch),
         pyglet.sprite.Sprite(self.img_music, x=525, y=315, batch=batch),
         pyglet.sprite.Sprite(self.img_shutdown, x=60, y=425, batch=batch),
         pyglet.sprite.Sprite(self.img_reboot, x=60, y=305, batch=batch),
         pyglet.sprite.Sprite(self.img_eq, x=60, y=185, batch=batch)]
         
  if(self.canPlay == True and self.canMute == True):
   self.controlSprites = [pyglet.sprite.Sprite(self.img_mute, x=50, y=108, batch=batch),
       pyglet.sprite.Sprite(self.img_prev, x=50, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_rewind, x=146, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_play, x=242, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_ff, x=338, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_next, x=434, y=35, batch=batch)]
       
  if(self.canPlay == False and self.canMute == True):
   self.controlSprites = [pyglet.sprite.Sprite(self.img_mute, x=50, y=108, batch=batch),
       pyglet.sprite.Sprite(self.img_prev, x=50, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_rewind, x=146, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_pause, x=242, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_ff, x=338, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_next, x=434, y=35, batch=batch)]
       
  if(self.canPlay == False and self.canMute == False):
   self.controlSprites = [pyglet.sprite.Sprite(self.img_unmute, x=50, y=108, batch=batch),
       pyglet.sprite.Sprite(self.img_prev, x=50, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_rewind, x=146, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_pause, x=242, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_ff, x=338, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_next, x=434, y=35, batch=batch)]
       
  if(self.canPlay == True and self.canMute == False):
   self.controlSprites = [pyglet.sprite.Sprite(self.img_unmute, x=50, y=108, batch=batch),
       pyglet.sprite.Sprite(self.img_prev, x=50, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_rewind, x=146, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_play, x=242, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_ff, x=338, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_next, x=434, y=35, batch=batch)]
       
  
  self.volumeSprites = [pyglet.sprite.Sprite(self.img_volume, x=498, y=35, batch=batch),
       pyglet.sprite.Sprite(self.currentFill, x=498, y=3, batch=batch)]
       
  
       
 def update_volume_fill(self):
  self.currentFill = self.img_volume_fill.get_region(x=0, y=0, width=int(self.img_volume_fill.width * self.currentVolume), height=64)
 
 def update_volume(self):
  self.player.volume = self.currentVolume
  
 def on_draw(self):
  self.clear()
  
  if(self.frameCount%10 == 0):
   #print("Resetting frame count")
   self.frameCount = 0
   
   self.update_volume_fill()
   
   self.update_volume()
   
   self.update_sprites()
  batch.draw()
  
  self.frameCount += 1
 
 def open_file(self):
  
  self.root = Tkinter.Tk()
  self.root.withdraw()
  
  if(self.mediaType == 'Audio'):
   f = tkFileDialog.askopenfilename(parent = self.root, title = 'Please select an audio file:', filetypes = [('MP3 Files', '.mp3'), ('WAV Files', '.wav'), ('All Files', '.*')], initialdir = 'F:\\Media\\Music\\')
   if(f):
    file = f
    self.root.destroy()
    return file
   else:
    self.root.destroy()
    return None
    
  else:
   f = tkFileDialog.askopenfilename(parent = self.root, title = 'Please select a video file:', filetypes = [('AVI Files', '.avi'), ('MPEG Files', '.mpeg'), ('MKV Files', '.mkv'), ('All Files', '.*')], initialdir = 'F:\\Media\\Video\\')
   if(f):
    file = f
    self.root.destroy()
    return file
   else:
    self.root.destroy()
    return None
  
  
 def refresh_player(self):
 
  self.player = pyglet.media.Player()
  
  @self.player.event('on_eos')
  def auto_next():
   self.next()
   
  self.player.volume = self.currentVolume
 
 def previous(self):
  #Go back to the previous file in the current directory
  
  self.player.canPlay = True
  self.player.pause()
  if(self.player.source != 'null'):
   
   #Find the index of the first / from the end of our source filepath
   sourceIndex = self.selectedFile.rfind('/')
   
   #Now we can get the name of the directory (including path)
   directory = self.selectedFile[:sourceIndex + 1]
   
   previousFile = 'null'
   if(self.mediaType == 'Audio'):
    for file in os.listdir(directory):
     if file.endswith('.MP3') or file.endswith('.mp3'):
      print('Previous Button: Checking ' + file)
      if(self.selectedFile == (directory + file)):
       print('Previous Button: Match!')
       #Found the current file, now we need to check if there is a file before this in the directory, and play that file if there is
       if(previousFile != 'null'):
        print('Previous Button: Previous file is ' + previousFile)
        
        self.refresh_player()
        
        media = pyglet.media.load(directory + previousFile)
        self.player.queue(media)
        
        self.selectedFile = directory + previousFile
        
        self.player.play()
        self.player.canPlay = False
       else:
        print('Previous Button: Current file first in directory!')
        
       return
      else:
       #Assign the current file to previousFile so that we can refer back to it if the next file is the current source
       previousFile = file
       
     if file.endswith('.WAV') or file.endswith('.wav') or file.endswith('.Wav') or file.endswith('.WAv') or file.endswith('.waV') or file.endswith('.wAV'):
      print('Previous Button: Checking ' + file)
      if(self.selectedFile == (directory + file)):
       print('Previous Button: Match!')
       #Found the current file, now we need to check if there is a file before this in the directory, and play that file if there is
       if(previousFile != 'null'):
        print('Previous Button: Previous file is ' + previousFile)
        
        self.refresh_player()
        
        media = pyglet.media.load(directory + previousFile)
        self.player.queue(media)
        
        self.selectedFile = directory + previousFile
        
        self.player.play()
        self.player.canPlay = False
       else:
        print('Previous Button: Current file first in directory!')
        
       return
      else:
       #Assign the current file to previousFile so that we can refer back to it if the next file is the current source
       previousFile = file
       
  else:
   print('Previous Button: Source is null!')
        
 def next(self):
  #Go to the next file in the current directory
  
  self.player.canPlay = True
  self.player.pause()
  if(self.player.source != 'null'):
   
   #Find the index of the first / from the end of our source filepath
   sourceIndex = self.selectedFile.rfind('/')
   
   #Now we can get the name of the directory (including path)
   directory = self.selectedFile[:sourceIndex + 1]
   
   foundCurrent = False
   isLastFile = True
   
   if(self.mediaType == 'Audio'):
    for file in os.listdir(directory):
     if file.endswith('.MP3') or file.endswith('.mp3') or file.endswith('.mP3') or file.endswith('.Mp3'):
      if(foundCurrent):
       print('Next Button: Next file is ' + file)
       
       self.refresh_player()
       
       media = pyglet.media.load(directory + file)
       self.player.queue(media)
       
       self.selectedFile = (directory + file)
       
       self.player.play()
       self.player.canPlay = False
       
       #Use this to tell program we successfully loaded the next file
       isLastFile = False
       
       return
       
      if(self.selectedFile == (directory + file)):
      
       #Found the current file, so change foundCurrent to True
       foundCurrent = True
       
     if file.endswith('.WAV') or file.endswith('.wav') or file.endswith('.Wav') or file.endswith('.WAv') or file.endswith('.waV') or file.endswith('.wAV'):
      if(foundCurrent):
       print('Next Button: Next file is ' + file)
       
       self.refresh_player()
       
       media = pyglet.media.load(directory + file)
       self.player.queue(media)
       
       self.selectedFile = (directory + file)
       
       self.player.play()
       self.player.canPlay = False
       
       #Use this to tell program we successfully loaded the next file
       isLastFile = False
       
       return
       
      if(self.selectedFile == (directory + file)):
      
       #Found the current file, so change foundCurrent to True
       foundCurrent = True
        
   if(isLastFile):
    print('Next Button: Current file is last in the directory!')
      
   
 def check_for_button(self, x, y):
  #Check all button coordinates against x and y, return True if a button was hit or False if not
  
  if( (525 - (self.img_music.width / 2)) < x < (525 + (self.img_music.width / 2)) and ( (315 - (self.img_music.height / 2)) < y < (315 + (self.img_music.height / 2)) ) ):
   #coordinate lies inside the music button
   if(self.player.playing):
    self.player.canPlay = True
    self.player.pause()
    wasPlaying = True
   else:
    wasPlaying = False
   
   self.mediaType = 'Audio'
   filename = self.open_file()
   
   if(filename):
    self.selectedFile = filename
    print('Opening ' + self.selectedFile + ' ...')
    
    #Drop the current queue so we can build a new one by overwriting our player with a fresh one
    self.refresh_player()
    
    media = pyglet.media.load(filename)
    self.player.queue(media)
    
  
    #Start playback from the selected file
    self.canPlay = False
    self.player.play()
   else:
    print('Dialog cancelled')
    
    if(wasPlaying):
     self.player.play()
   
  if( (280 - (self.img_movies.width / 2)) < x < (280 + (self.img_movies.width / 2)) and ( (315 - (self.img_movies.height /2)) < y < (315 + (self.img_movies.height /2)) ) ):
   #coordinate lies inside the movies button
   if(self.player.playing):
    self.player.canPlay = True
    self.player.pause()
    wasPlaying = True
   else:
    wasPlaying = False
   
   self.mediaType = 'Video'
   filename = self.open_file()
   
   if(filename):
    self.selectedFile = filename
    print('Opening ' + self.selectedFile + ' ...')
    
    #Drop the current queue so we can build a new one by overwriting our player with a fresh one
    self.refresh_player()
    
    media = pyglet.media.load(filename)
    self.player.queue(media)
  
    
    #Start playback from the selected file
    self.canPlay = False
    self.player.play()
   else:
    print('Dialog cancelled')
    
    if(wasPlaying):
     self.player.play()
    
  if( (242 - (self.img_play.width / 2)) < x < (242 + (self.img_play.width / 2)) and ( (35 - (self.img_play.height / 2)) < y < (35 + (self.img_play.height / 2)) ) ):
   #coordinate lies inside the play/pause button
   if(self.canPlay):
    print('Play Button: Playing ' + self.selectedFile)
    self.player.play()
    
    self.canPlay = False
   else:    
    print('Pause Button: Pausing ' + self.selectedFile)
    self.player.pause()
    
    self.canPlay = True
   
  if( (434 - (self.img_next.width / 2)) < x < (434 + (self.img_next.width / 2)) and ( (35 - (self.img_next.height / 2)) < y < (35 + (self.img_next.height / 2)) ) ):
   #coordinate lies inside the next button
   print('Clicked next button...')
   
   self.next()
   
  if( (50 - (self.img_prev.width / 2)) < x < (50 + (self.img_prev.width / 2)) and ( (35 - (self.img_prev.height / 2)) < y < (35 + (self.img_prev.height / 2)) ) ):
   #coordinate lies inside the previous button
   print('Clicked previous button...')
   
   self.previous()
   
  if( (50 - (self.img_mute.width / 2)) < x < (50 + (self.img_mute.width / 2)) and ( (108 - (self.img_mute.height / 2)) < y < (108 + (self.img_mute.height / 2)) ) ):
   #coordinate lies inside the mute/unmute button
   if(self.canMute):
    print('Mute Button: Audio Muted!')
    self.player.volume = 0
    self.canMute = False
   else:
    print('Mute Button: Audio Unmuted!')
    self.player.volume = self.currentVolume
    self.canMute = True
    
  if( (146 - (self.img_rewind.width / 2)) < x < (146 + (self.img_rewind.width / 2)) and ( (35 - (self.img_rewind.height / 2)) < y < (35 + (self.img_rewind.height / 2)) ) ):
   #coordinate lies inside the rewind button
   if(self.player.playing or self.canPlay):
    print('Rewind Button: Seeking backwards 10 seconds.')
    newPosition = self.player.time - 10.0
    
    if(newPosition < 0.0):
     print('Rewind Button: New time is negative, set to 0 instead.')
     newPosition = 0.0
     
    self.player.seek(newPosition)
    
    if(self.canPlay):
     pass
    else:
     self.player.play()
    
  if( (338 - (self.img_ff.width / 2)) < x < (338 + (self.img_ff.width / 2)) and ( (35 - (self.img_ff.height / 2)) < y < (35 + (self.img_ff.height / 2)) ) ):
   #coordinate lies inside the fast forward button
   if(self.player.playing or self.canPlay):
    print('FF Button: Seeking forwards 10 seconds.')
    newPosition = self.player.time + 10.0
    
    #Note: We don't need to check if the time is past the end of the track because the Player() class will automatically clamp this to the end of the source
    
    self.player.seek(newPosition)
    if(self.canPlay):
     pass
    else:
     self.player.play()
     
     
  self.volumeSprites = [pyglet.sprite.Sprite(self.img_volume, x=498, y=35, batch=batch),
       pyglet.sprite.Sprite(self.img_volume_fill, x=498, y=35, batch=batch)]
       
  if( (498 < x < (498 + self.img_volume.width)) and ( (35 - (self.img_volume.height / 2)) < y < (35 + (self.img_volume.height / 2)) ) ):
   #coordinate lies inside the volume control
   
   #First, we need to convert the image's width to a percentage scale
   ratio = 100.0 / self.img_volume.width / 100.0
   
   #To make more sense of this, here is an example:
   #100 / 256 = 0.390625
   #If our touch/mouse location was 128 (right in the middle of the image), we want to set our volume to 50%, or 0.5
   #Since 128 * 0.390625 = 50, we still need to divide that by 100 to get the percentage (or 0 to 1 decimal) that we want to use.
   #Therefore, instead of 100 / 256 = 0.390625, we need a ratio of 100 / 256 / 100 = 0.00390625
   
   #Now we need to find our x position in relation to the image, instead of the screen like it currently is
   local_x = x - 498
   
   
   #Let's use our new conversion factor to set the volume appropriately
   self.currentVolume = local_x * ratio
   
   
   
 
 def on_mouse_press(self, x, y, button, modifiers):
  self.check_for_button(x, y)
  
  
if __name__ == '__main__':
 window = Piamp()
 #window.set_mouse_visible(False)
 pyglet.app.run()


I've decided to utilize Tkinter and tkFileDialog for the file opening window, which looks like this:

This dialog will look different depending on the operating system that runs it. Tkinter works on a variety of operating systems, so it was an easy choice for this functionality.

To give you an idea on the state of the player:

Working:

  • Volume Slider
  • Previous Button
  • Rewind
  • Play/Pause
  • Fast Forward
  • Next Button
  • Mute/Unmute
  • Music Button (Open audio file)
  • Movies Button (Open video file)
Not Yet Implemented:
  • Show current selected file in text box
  • Shutdown button
  • Reboot button
  • Equalizer button
  • Video Playback - Currently player only plays audio for video files
In case you want to play around with the player (or the code) yourself, here is a zip file of the entire project:


Stay tuned - I'll be finishing up as much as possible tomorrow, and then I'm transferring all of this to the Pi and getting it installed in the car!

No comments:

Post a Comment