# 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