BAPSicle/player.py

405 lines
12 KiB
Python
Raw Normal View History

# 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.
2020-10-29 21:23:37 +00:00
from queue import Empty
import multiprocessing
import setproctitle
import copy
import json
import time
2020-11-01 00:31:58 +00:00
import sys
# Stop the Pygame Hello message.
import os
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"
2020-10-29 21:23:37 +00:00
from pygame import mixer
2020-10-25 01:23:24 +00:00
from mutagen.mp3 import MP3
from helpers.state_manager import StateManager
from helpers.logging_manager import LoggingManager
2020-10-23 20:10:32 +00:00
2020-10-24 20:31:52 +00:00
class Player():
state = None
running = False
2020-10-30 00:32:34 +00:00
out_q = None
last_msg = None
logger = None
2020-10-24 20:31:52 +00:00
__default_state = {
2020-10-30 00:32:34 +00:00
"initialised": False,
2020-10-24 20:31:52 +00:00
"filename": "",
"channel": -1,
"playing": False,
"paused": False,
2020-10-29 22:25:17 +00:00
"loaded": False,
2020-10-24 20:31:52 +00:00
"pos": 0,
"pos_offset": 0,
"pos_true": 0,
2020-10-24 20:31:52 +00:00
"remaining": 0,
"length": 0,
"loop": False,
"output": None
}
2020-10-29 22:25:17 +00:00
@property
2020-10-24 20:31:52 +00:00
def isInit(self):
try:
2020-10-25 01:23:24 +00:00
mixer.music.get_busy()
2020-10-24 20:31:52 +00:00
except:
return False
2020-10-29 22:25:17 +00:00
return True
2020-10-24 20:31:52 +00:00
2020-10-29 21:23:37 +00:00
@property
2020-10-24 20:31:52 +00:00
def isPlaying(self):
2020-10-29 22:25:17 +00:00
if self.isInit:
return (not self.isPaused) and bool(mixer.music.get_busy())
2020-10-29 22:25:17 +00:00
return False
2020-10-24 20:31:52 +00:00
@property
def isPaused(self):
return self.state.state["paused"]
2020-10-29 21:23:37 +00:00
@property
def isLoaded(self):
2020-10-29 22:25:17 +00:00
if not self.state.state["filename"]:
return False
if self.isPlaying:
return True
2020-10-30 00:32:34 +00:00
# Because Pygame/SDL is annoying
# We're not playing now, so we can quickly test run
# If that works, we're loaded.
2020-10-29 22:25:17 +00:00
try:
2020-10-30 00:32:34 +00:00
position = self.state.state["pos"]
mixer.music.set_volume(0)
mixer.music.play(0)
2020-10-29 22:25:17 +00:00
except:
2020-10-30 00:32:34 +00:00
try:
mixer.music.set_volume(1)
except:
2020-11-03 22:48:11 +00:00
self.logger.log.exception("Failed to reset volume after attempting loaded test.")
2020-10-30 00:32:34 +00:00
pass
2020-10-29 22:25:17 +00:00
return False
2020-10-30 00:32:34 +00:00
if position > 0:
self.pause()
2020-10-30 00:32:34 +00:00
else:
self.stop()
2020-10-30 00:32:34 +00:00
mixer.music.set_volume(1)
return True
2020-10-29 22:25:17 +00:00
2020-10-30 00:32:34 +00:00
@property
def status(self):
res = json.dumps(self.state.state)
return res
2020-10-29 21:23:37 +00:00
def play(self, pos=0):
try:
mixer.music.play(0, pos)
self.state.update("pos_offset", pos)
except:
2020-11-03 22:48:11 +00:00
self.logger.log.exception("Failed to play at pos: " + str(pos))
return False
self.state.update("paused", False)
return True
2020-10-24 20:31:52 +00:00
def pause(self):
try:
mixer.music.pause()
except:
2020-11-03 22:48:11 +00:00
self.logger.log.exception("Failed to pause.")
return False
self.state.update("paused", True)
return True
2020-10-24 20:31:52 +00:00
def unpause(self):
2020-10-29 22:25:17 +00:00
if not self.isPlaying:
2020-11-03 22:48:11 +00:00
position = self.state.state["pos_true"]
2020-10-29 22:25:17 +00:00
try:
2020-11-03 22:48:11 +00:00
self.play(position)
2020-10-29 22:25:17 +00:00
except:
2020-11-03 22:48:11 +00:00
self.logger.log.exception("Failed to unpause from pos: " + str(position))
2020-10-29 22:25:17 +00:00
return False
self.state.update("paused", False)
2020-10-29 22:25:17 +00:00
return True
return False
2020-10-24 20:31:52 +00:00
def stop(self):
# if self.isPlaying or self.isPaused:
try:
mixer.music.stop()
except:
2020-11-03 22:48:11 +00:00
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
2020-10-24 20:31:52 +00:00
def seek(self, pos):
2020-10-29 22:25:17 +00:00
if self.isPlaying:
try:
self.play(pos)
2020-10-29 22:25:17 +00:00
except:
2020-11-03 22:48:11 +00:00
self.logger.log.exception("Failed to seek to pos: " + str(pos))
2020-10-29 22:25:17 +00:00
return False
return True
else:
self.state.update("paused", True)
self._updateState(pos=pos)
return True
2020-10-24 20:31:52 +00:00
def load(self, filename):
2020-10-29 22:25:17 +00:00
if not self.isPlaying:
self.unload()
# Fix any OS specific / or \'s
if os.path.sep == "/":
filename = filename.replace("\\", '/')
else:
filename = filename.replace("/", '\\')
2020-10-29 22:25:17 +00:00
2020-10-24 20:31:52 +00:00
self.state.update("filename", filename)
2020-10-29 22:25:17 +00:00
try:
2020-11-03 22:48:11 +00:00
self.logger.log.info("Loading file: " + str(filename))
2020-10-29 22:25:17 +00:00
mixer.music.load(filename)
except:
# We couldn't load that file.
2020-11-03 22:48:11 +00:00
self.logger.log.exception("Couldn't load file: " + str(filename))
2020-10-29 22:25:17 +00:00
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:
2020-11-03 22:48:11 +00:00
self.logger.log.exception("Failed to update the length of item.")
2020-10-29 22:25:17 +00:00
return False
return True
def unload(self):
if not self.isPlaying:
try:
mixer.music.unload()
self.state.update("paused", False)
2020-10-29 22:25:17 +00:00
self.state.update("filename", "")
except:
2020-11-03 22:48:11 +00:00
self.logger.log.exception("Failed to unload channel.")
2020-10-29 22:25:17 +00:00
return False
return not self.isLoaded
2020-10-24 20:31:52 +00:00
def quit(self):
2020-11-03 22:48:11 +00:00
try:
mixer.quit()
self.state.update("paused", False)
except:
self.logger.log.exception("Failed to quit mixer.")
2020-10-24 20:31:52 +00:00
def output(self, name=None):
2020-11-01 03:19:21 +00:00
wasPlaying = self.state.state["playing"]
2020-10-24 20:31:52 +00:00
self.quit()
2020-10-30 00:32:34 +00:00
self.state.update("output", name)
2020-10-24 20:31:52 +00:00
try:
if name:
2020-11-01 03:27:26 +00:00
mixer.init(44100, -16, 2, 1024, devicename=name)
2020-10-24 20:31:52 +00:00
else:
2020-11-01 03:27:26 +00:00
mixer.init(44100, -16, 2, 1024)
2020-10-24 20:31:52 +00:00
except:
2020-11-03 22:48:11 +00:00
self.logger.log.exception("Failed to init mixer with device name: " + str(name))
2020-10-30 00:32:34 +00:00
return False
2020-10-24 20:31:52 +00:00
self.load(self.state.state["filename"])
2020-11-01 03:19:21 +00:00
if wasPlaying:
self.unpause()
2020-10-30 00:32:34 +00:00
return True
2020-10-24 20:31:52 +00:00
2020-10-30 00:32:34 +00:00
def _updateState(self, pos=None):
self.state.update("initialised", self.isInit)
if self.isInit:
if 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))
2020-10-30 00:32:34 +00:00
self.state.update("playing", self.isPlaying)
self.state.update("loaded", self.isLoaded)
2020-10-30 00:32:34 +00:00
if (pos):
self.state.update("pos", max(0, pos))
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"])
2020-10-30 00:32:34 +00:00
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
2020-10-24 20:31:52 +00:00
else:
2020-10-30 00:32:34 +00:00
response += "FAIL"
if self.out_q:
self.out_q.put(response)
2020-10-24 20:31:52 +00:00
def __init__(self, channel, in_q, out_q):
2020-10-30 00:32:34 +00:00
process_title = "Player: Channel " + str(channel)
setproctitle.setproctitle(process_title)
multiprocessing.current_process().name = process_title
2020-10-24 20:31:52 +00:00
self.running = True
self.out_q = out_q
2020-10-24 20:31:52 +00:00
self.logger = LoggingManager("channel" + str(channel))
2020-10-30 23:59:58 +00:00
self.state = StateManager("channel" + str(channel), self.logger, self.__default_state)
self.state.update("channel", channel)
2020-10-24 20:31:52 +00:00
loaded_state = copy.copy(self.state.state)
if loaded_state["output"]:
self.logger.log.info("Setting output to: " + loaded_state["output"])
2020-10-24 20:31:52 +00:00
self.output(loaded_state["output"])
else:
self.logger.log.info("Using default output device.")
2020-10-24 20:31:52 +00:00
self.output()
if loaded_state["filename"]:
self.logger.log.info("Loading filename: " + loaded_state["filename"])
2020-10-24 20:31:52 +00:00
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"])
2020-10-24 20:31:52 +00:00
2020-10-29 21:23:37 +00:00
if loaded_state["playing"] == True:
self.logger.log.info("Resuming.")
2020-10-29 21:23:37 +00:00
self.unpause()
else:
self.logger.log.info("No file was previously loaded.")
2020-10-24 20:31:52 +00:00
while self.running:
2020-10-30 00:32:34 +00:00
time.sleep(0.1)
self._updateState()
2020-10-29 21:23:37 +00:00
try:
try:
2020-10-30 00:32:34 +00:00
self.last_msg = in_q.get_nowait()
2020-10-29 21:23:37 +00:00
except Empty:
# The incomming message queue was empty,
# skip message processing
pass
else:
2020-10-30 00:32:34 +00:00
2020-10-29 21:23:37 +00:00
# We got a message.
2020-10-29 22:25:17 +00:00
2020-10-30 00:32:34 +00:00
# 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:
2020-10-29 21:23:37 +00:00
2020-10-30 00:32:34 +00:00
if (self.last_msg == 'LOADED?'):
self._retMsg(self.isLoaded)
2020-10-29 21:23:37 +00:00
continue
2020-10-30 00:32:34 +00:00
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'):
2020-10-29 21:23:37 +00:00
self.running = False
2020-10-30 00:32:34 +00:00
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)
2020-10-29 21:23:37 +00:00
# Catch the player being killed externally.
except KeyboardInterrupt:
2020-11-03 22:48:11 +00:00
self.logger.log.info("Received KeyboardInterupt")
2020-10-29 21:23:37 +00:00
break
except SystemExit:
2020-11-03 22:48:11 +00:00
self.logger.log.info("Received SystemExit")
2020-10-29 21:23:37 +00:00
break
except:
2020-11-03 22:48:11 +00:00
self.logger.log.exception("Received unexpected exception.")
break
2020-10-24 20:31:52 +00:00
self.logger.log.info("Quiting player ", channel)
2020-10-29 21:23:37 +00:00
self.quit()
2020-10-30 00:32:34 +00:00
self._retMsg("EXIT")
2020-11-01 00:31:58 +00:00
sys.exit(0)
2020-10-29 21:23:37 +00:00
def showOutput(in_q, out_q):
print("Starting showOutput().")
while True:
time.sleep(0.01)
last_msg = out_q.get()
print(last_msg)
2020-10-29 21:23:37 +00:00
if __name__ == "__main__":
if isMacOS:
multiprocessing.set_start_method("spawn", True)
2020-10-29 21:23:37 +00:00
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
2020-10-29 22:25:17 +00:00
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")
2020-10-29 21:23:37 +00:00
print("Entering infinite loop.")
while True:
pass