BAPSicle/player.py

413 lines
12 KiB
Python

# This is the player. Reliability is critical here, so we're catching
# literally every exception possible and handling it.
# It is key that whenever the parent server tells us to do something
# that we respond with something, FAIL or OKAY. The server doesn't like to be kept waiting.
from queue import Empty
import multiprocessing
import setproctitle
import copy
import json
import time
import sys
# Stop the Pygame Hello message.
import os
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
from pygame import mixer
from mutagen.mp3 import MP3
from helpers.state_manager import StateManager
from helpers.logging_manager import LoggingManager
class Player():
state = None
running = False
out_q = None
last_msg = None
logger = None
__default_state = {
"initialised": False,
"filename": "",
"channel": -1,
"playing": False,
"paused": False,
"loaded": False,
"pos": 0,
"pos_offset": 0,
"pos_true": 0,
"remaining": 0,
"length": 0,
"loop": False,
"output": None
}
__rate_limited_params = [
"pos",
"pos_offset",
"pos_true",
"remaining"
]
@property
def isInit(self):
try:
mixer.music.get_busy()
except:
return False
return True
@property
def isPlaying(self):
if self.isInit:
return (not self.isPaused) and bool(mixer.music.get_busy())
return False
@property
def isPaused(self):
return self.state.state["paused"]
@property
def isLoaded(self):
if not self.state.state["filename"]:
return False
if self.isPlaying:
return True
# Because Pygame/SDL is annoying
# We're not playing now, so we can quickly test run
# If that works, we're loaded.
try:
position = self.state.state["pos"]
mixer.music.set_volume(0)
mixer.music.play(0)
except:
try:
mixer.music.set_volume(1)
except:
self.logger.log.exception("Failed to reset volume after attempting loaded test.")
pass
return False
if position > 0:
self.pause()
else:
self.stop()
mixer.music.set_volume(1)
return True
@property
def status(self):
res = json.dumps(self.state.state)
return res
def play(self, pos=0):
try:
mixer.music.play(0, pos)
self.state.update("pos_offset", pos)
except:
self.logger.log.exception("Failed to play at pos: " + str(pos))
return False
self.state.update("paused", False)
return True
def pause(self):
try:
mixer.music.pause()
except:
self.logger.log.exception("Failed to pause.")
return False
self.state.update("paused", True)
return True
def unpause(self):
if not self.isPlaying:
position = self.state.state["pos_true"]
try:
self.play(position)
except:
self.logger.log.exception("Failed to unpause from pos: " + str(position))
return False
self.state.update("paused", False)
return True
return False
def stop(self):
# if self.isPlaying or self.isPaused:
try:
mixer.music.stop()
except:
self.logger.log.exception("Failed to stop playing.")
return False
self.state.update("pos", 0)
self.state.update("pos_offset", 0)
self.state.update("pos_true", 0)
self.state.update("paused", False)
return True
# return False
def seek(self, pos):
if self.isPlaying:
try:
self.play(pos)
except:
self.logger.log.exception("Failed to seek to pos: " + str(pos))
return False
return True
else:
self.state.update("paused", True)
self._updateState(pos=pos)
return True
def load(self, filename):
if not self.isPlaying:
self.unload()
# Fix any OS specific / or \'s
if os.path.sep == "/":
filename = filename.replace("\\", '/')
else:
filename = filename.replace("/", '\\')
self.state.update("filename", filename)
try:
self.logger.log.info("Loading file: " + str(filename))
mixer.music.load(filename)
except:
# We couldn't load that file.
self.logger.log.exception("Couldn't load file: " + str(filename))
return False
try:
if ".mp3" in filename:
song = MP3(filename)
self.state.update("length", song.info.length)
else:
self.state.update("length", mixer.Sound(filename).get_length()/1000)
except:
self.logger.log.exception("Failed to update the length of item.")
return False
return True
def unload(self):
if not self.isPlaying:
try:
mixer.music.unload()
self.state.update("paused", False)
self.state.update("filename", "")
except:
self.logger.log.exception("Failed to unload channel.")
return False
return not self.isLoaded
def quit(self):
try:
mixer.quit()
self.state.update("paused", False)
except:
self.logger.log.exception("Failed to quit mixer.")
def output(self, name=None):
wasPlaying = self.state.state["playing"]
self.quit()
self.state.update("output", name)
try:
if name:
mixer.init(44100, -16, 2, 1024, devicename=name)
else:
mixer.init(44100, -16, 2, 1024)
except:
self.logger.log.exception("Failed to init mixer with device name: " + str(name))
return False
self.load(self.state.state["filename"])
if wasPlaying:
self.unpause()
return True
def _updateState(self, pos=None):
self.state.update("initialised", self.isInit)
if self.isInit:
if (pos):
self.state.update("pos", max(0, pos))
elif self.isPlaying:
# Get one last update in, incase we're about to pause/stop it.
self.state.update("pos", max(0, mixer.music.get_pos()/1000))
self.state.update("playing", self.isPlaying)
self.state.update("loaded", self.isLoaded)
self.state.update("pos_true", self.state.state["pos"] + self.state.state["pos_offset"])
self.state.update("remaining", self.state.state["length"] - self.state.state["pos_true"])
def _retMsg(self, msg, okay_str=False):
response = self.last_msg + ":"
if msg == True:
response += "OKAY"
elif isinstance(msg, str):
if okay_str:
response += "OKAY:" + msg
else:
response += "FAIL:" + msg
else:
response += "FAIL"
if self.out_q:
self.out_q.put(response)
def __init__(self, channel, in_q, out_q):
process_title = "Player: Channel " + str(channel)
setproctitle.setproctitle(process_title)
multiprocessing.current_process().name = process_title
self.running = True
self.out_q = out_q
self.logger = LoggingManager("channel" + str(channel))
self.state = StateManager("channel" + str(channel), self.logger, self.__default_state, self.__rate_limited_params)
self.state.update("channel", channel)
loaded_state = copy.copy(self.state.state)
if loaded_state["output"]:
self.logger.log.info("Setting output to: " + loaded_state["output"])
self.output(loaded_state["output"])
else:
self.logger.log.info("Using default output device.")
self.output()
if loaded_state["filename"]:
self.logger.log.info("Loading filename: " + loaded_state["filename"])
self.load(loaded_state["filename"])
if loaded_state["pos_true"] != 0:
self.logger.log.info("Seeking to pos_true: " + str(loaded_state["pos_true"]))
self.seek(loaded_state["pos_true"])
if loaded_state["playing"] == True:
self.logger.log.info("Resuming.")
self.unpause()
else:
self.logger.log.info("No file was previously loaded.")
while self.running:
time.sleep(0.1)
self._updateState()
try:
try:
self.last_msg = in_q.get_nowait()
except Empty:
# The incomming message queue was empty,
# skip message processing
pass
else:
# We got a message.
# Output re-inits the mixer, so we can do this any time.
if (self.last_msg.startswith("OUTPUT")):
split = self.last_msg.split(":")
self._retMsg(self.output(split[1]))
elif self.isInit:
if (self.last_msg == 'LOADED?'):
self._retMsg(self.isLoaded)
continue
elif (self.last_msg == 'PLAY'):
self._retMsg(self.play())
elif (self.last_msg == 'PAUSE'):
self._retMsg(self.pause())
elif (self.last_msg == 'UNPAUSE'):
self._retMsg(self.unpause())
elif (self.last_msg == 'STOP'):
self._retMsg(self.stop())
elif (self.last_msg == 'QUIT'):
self.running = False
continue
elif (self.last_msg.startswith("SEEK")):
split = self.last_msg.split(":")
self._retMsg(self.seek(float(split[1])))
elif (self.last_msg.startswith("LOAD")):
split = self.last_msg.split(":")
self._retMsg(self.load(split[1]))
elif (self.last_msg == 'UNLOAD'):
self._retMsg(self.unload())
elif (self.last_msg == 'STATUS'):
self._retMsg(self.status, True)
else:
self._retMsg("Unknown Command")
else:
if (self.last_msg == 'STATUS'):
self._retMsg(self.status)
else:
self._retMsg(False)
# Catch the player being killed externally.
except KeyboardInterrupt:
self.logger.log.info("Received KeyboardInterupt")
break
except SystemExit:
self.logger.log.info("Received SystemExit")
break
except:
self.logger.log.exception("Received unexpected exception.")
break
self.logger.log.info("Quiting player ", channel)
self.quit()
self._retMsg("EXIT")
sys.exit(0)
def showOutput(in_q, out_q):
print("Starting showOutput().")
while True:
time.sleep(0.01)
last_msg = out_q.get()
print(last_msg)
if __name__ == "__main__":
if isMacOS:
multiprocessing.set_start_method("spawn", True)
in_q = multiprocessing.Queue()
out_q = multiprocessing.Queue()
outputProcess = multiprocessing.Process(
target=showOutput,
args=(in_q, out_q),
).start()
playerProcess = multiprocessing.Process(
target=Player,
args=(-1, in_q, out_q),
).start()
# Do some testing
in_q.put("LOADED?")
in_q.put("PLAY")
in_q.put("LOAD:\\Users\\matth\\Documents\\GitHub\\bapsicle\\dev\\test.mp3")
in_q.put("LOADED?")
in_q.put("PLAY")
print("Entering infinite loop.")
while True:
pass