-
- import os
- import StringIO
- import pymedia
- import threading
- import logging
-
- # Audio options
- WAV = {
- "bytes_minimum":4096,
- }
-
- MP3 = {
- "bytes_minimum":4096*2,
- }
-
- OGG = {
- "bytes_minimum":4096*4,
- }
-
-
- FORMATS = {
- ".wav":WAV,
- ".mp3":MP3,
- ".ogg":OGG,
- }
-
- __doc__ = """This library contains the Player class which is the threaded player for music/sfx playback with the backing of pymedia library."""
-
- class Player(threading.Thread):
- '''Simple media player written with threaded support based on pymedia.
-
- This threaded player will attempt to retrieve data from any kind of file object with seek
- and read method. It will then, based on the passed filename, decide which codec to use and
- demux + decode the data on the fly. It will require the constant FORMATS
- to be declared, with the number of bytes per tick to read.
-
- Player can be played, paused or stopped. However, the change will only manifest
- itself when the tick passed (ie enough data was processed). This is FORMATS based
- because different formats need different minimal data volume per block to success
- fully demux and decode.
-
- '''
-
- def __init__(self):
- '''Constructor'''
- threading.Thread.__init__(self)
-
- self.alive = True
- self.dec = None
- self.volume = 1.0
- self.repeat = True
- self.data = None
- self.finfoformat = None
- self.audioobj = None
- self.mplay = False
-
- def null():
- pass # empty call in case we do not need the exception_call
-
- self.exception_call = null
-
-
- def set_exception_callback(self, what):
- self.exception_call = what
-
-
- def __set_volume(self):
- """Internal volume setter.
-
- This method it called every tick to set the volume.
- """
- volume = int(100 * self.volume)
- vol = (volume << 8) + volume
- if vol > ((100 << 8) + 100):
- vol = (100 << 8) + 100
- elif vol < 0:
- vol = 0
- self.audioobj.setVolume(vol)
-
-
- def get_volume(self):
- """returns the volume.
-
- Returns:
-
- volume - 0xFFFF formated volume (100 is max).
-
- This volume is the volume of the sound mixer, not the track itself!
- """
- volume = self.audioobj.getVolume()
- return volume
-
-
- def is_alive(self):
- """returns the thread status."""
- return self.alive
-
-
- def change_repeat(self, repeatval):
- """changes the repeat of the music/sound playback.
-
- Parameters:
-
- repeatval - Boolean
- """
- self.repeat = repeatval
-
-
- def add_data(self, io, ext="foo.wav"):
- """loads the demuxer and Input/Output object.
-
- Parameters:
-
- io - File like object, with seek and read methods
-
- Optional parameters:
-
- ext - File name containing extension, used for initializing demuxer.
- """
- self.finfoformat = FORMATS[os.path.splitext(ext)[1]]
- self.dm = pymedia.muxer.Demuxer(str.split(ext,".")[-1].lower())
-
- self.data = io
- self.data.seek(0)
-
-
- def play(self):
- """starts the playback."""
- self.mplay = True
-
-
- def pause(self):
- """pauses the playback."""
- self.mplay = False
-
-
- def resume(self):
- """resumes the playback"""
- self.play()
-
-
- def stop(self):
- """suspends the playback and kills the thread."""
- self.pause()
- self.kill()
-
-
- def kill(self):
- """kills the thread."""
- self.alive = False
-
-
- def run(self):
- """main thread instance loop.
-
- This loop operates in ticks, with one tick being one round of the loop.
- The loop starts with loading the data from the IO source into the buffer
- then if the data is less than the full amount, it will either decide to
- reload the buffer or to kill the playback and the loop (based on the
- repeat value).
-
- Afterwards, it will try to demux the frames out of the loaded buffer
- and should it succeed, it will then play all the frames it was successful
- in the demuxing.
-
- The volume setter is set every time the frame is being played, so the fadeout
- should be working on the system that is supporting it (not supporting systems
- include PulseAudio or Alsa, but Windows seems to work).
-
- In this part of the loop, the audio playback device is instantiated, and should
- it fail with SoundError exception, it will be logged and thread will be promptly
- killed with a return. The callback exception will be called as well.
-
- If no problems were detected, the playback will continue until the whole IO object
- was used and either loop or end.
- """
- buf = ""
- while self.alive:
- if self.mplay:
- buf += self.data.read(self.finfoformat["bytes_minimum"])
- if len(buf) < self.finfoformat["bytes_minimum"]:
- if self.repeat:
- self.data.seek(0)
- else:
- self.kill()
- try:
- frames = self.dm.parse(buf)
- except:
- frames = None
- if frames:
- for fr in frames:
- if not self.mplay:
- break
- if self.dec is None:
- self.dec = pymedia.audio.acodec.Decoder(self.dm.streams[fr[0]])
- dat = self.dec.decode(fr[1])
- if dat is not None:
- if self.audioobj is None:
- try:
- self.audioobj = pymedia.audio.sound.Output(dat.sample_rate, dat.channels, pymedia.audio.sound.AFMT_S16_LE)
- except pymedia.audio.sound.SoundError, e:
- logging.error("Music thread failed with: %s" %str(e))
- self.exception_call()
- return
-
- self.__set_volume()
- self.audioobj.play(dat.data)
- buf = ""
-
-