mirror of
https://github.com/correl/turntable.git
synced 2024-12-26 19:16:58 +00:00
Added graceful shutdown handling
This commit is contained in:
parent
a7541084fa
commit
af2068490d
6 changed files with 90 additions and 30 deletions
|
@ -10,7 +10,7 @@ from typing import Any, Dict, Iterator, List, Optional
|
|||
from dejavu import Dejavu # type: ignore
|
||||
|
||||
from turntable.audio import Listener, Player
|
||||
from turntable.events import Event
|
||||
from turntable.events import Event, Exit
|
||||
from turntable.hue import Hue
|
||||
from turntable.icecast import Icecast
|
||||
from turntable.models import PCM
|
||||
|
@ -22,6 +22,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class Application:
|
||||
def __init__(self, events: "Queue[Event]", pcm: "Optional[Queue[PCM]]" = None):
|
||||
self.app_events: "Queue[Event]" = Queue()
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--config", default=os.path.expanduser("~/.config/turntable.json")
|
||||
|
@ -98,6 +99,7 @@ class Application:
|
|||
turntable_config = self.config.get("turntable", dict())
|
||||
turntable = Turntable(
|
||||
pcm_in,
|
||||
self.app_events,
|
||||
event_queues,
|
||||
listener.framerate,
|
||||
listener.channels,
|
||||
|
@ -128,7 +130,11 @@ class Application:
|
|||
process.start()
|
||||
|
||||
def shutdown(self) -> None:
|
||||
logging.info("Terminating")
|
||||
logging.info("Telling processes to exit")
|
||||
self.app_events.put(Exit())
|
||||
for process in self.processes:
|
||||
logging.debug("Waiting for %s to terminate", process)
|
||||
process.join(3)
|
||||
if process.is_alive():
|
||||
logging.info("Killing process %s", process)
|
||||
process.kill()
|
||||
|
|
|
@ -21,3 +21,7 @@ class StoppedPlaying(Event):
|
|||
@dataclass
|
||||
class NewMetadata(Event):
|
||||
title: str
|
||||
|
||||
|
||||
class Exit(Event):
|
||||
...
|
||||
|
|
|
@ -174,14 +174,13 @@ def main():
|
|||
app.run()
|
||||
clock = pygame.time.Clock()
|
||||
title = "<Idle>"
|
||||
while True:
|
||||
stopping = False
|
||||
while not stopping:
|
||||
for event in pygame.event.get():
|
||||
if event.type == QUIT or (
|
||||
event.type == KEYDOWN and event.key == K_ESCAPE
|
||||
):
|
||||
app.shutdown()
|
||||
pygame.quit()
|
||||
return
|
||||
stopping = True
|
||||
try:
|
||||
while event := event_queue.get(False):
|
||||
...
|
||||
|
@ -191,9 +190,12 @@ def main():
|
|||
title = "<Idle>"
|
||||
elif isinstance(event, events.NewMetadata):
|
||||
title = event.title
|
||||
|
||||
elif isinstance(event, events.Exit):
|
||||
stopping = True
|
||||
except queue.Empty:
|
||||
...
|
||||
if stopping:
|
||||
break
|
||||
try:
|
||||
while sample := pcm_in.get(False):
|
||||
plot.audio = sample
|
||||
|
@ -212,4 +214,6 @@ def main():
|
|||
except:
|
||||
logger.exception("Shutting down")
|
||||
finally:
|
||||
logger.info("Stopping GUI")
|
||||
pygame.quit()
|
||||
app.shutdown()
|
||||
|
|
|
@ -57,22 +57,24 @@ class Hue(Process):
|
|||
self.username = username
|
||||
self.light = light
|
||||
self.light_id = None
|
||||
self.light_state = dict()
|
||||
self.active = False
|
||||
|
||||
try:
|
||||
lights = hue_response(
|
||||
requests.get(f"http://{self.host}/api/{self.username}/lights")
|
||||
)
|
||||
except HueError as error:
|
||||
logger.warn(f"Error fetching lights: {error}")
|
||||
logger.warn(f"Error fetching lights: %s", error)
|
||||
return
|
||||
try:
|
||||
self.light_id = next(
|
||||
self.light_id, self.light_state = next(
|
||||
filter(
|
||||
lambda i: i[1]["name"].lower() == self.light.lower(), lights.items()
|
||||
)
|
||||
)[0]
|
||||
)
|
||||
except StopIteration:
|
||||
logger.warn(f"Could not find a light named '{light}")
|
||||
logger.warn(f"Could not find a light named '%s'", light)
|
||||
return
|
||||
logger.info("Hue ready")
|
||||
|
||||
|
@ -83,33 +85,61 @@ class Hue(Process):
|
|||
logger.debug("Starting Hue")
|
||||
max_peak = 3000
|
||||
audio = None
|
||||
while True:
|
||||
stopping = False
|
||||
while not stopping:
|
||||
try:
|
||||
while event := self.events.get(False):
|
||||
...
|
||||
if isinstance(event, StartedPlaying):
|
||||
try:
|
||||
self.light_state = hue_response(
|
||||
requests.get(
|
||||
f"http://{self.host}/api/{self.username}/lights/{self.light_id}"
|
||||
)
|
||||
)
|
||||
logger.debug("Stored light state")
|
||||
except HueError as e:
|
||||
logger.warn(f"Error loading current light state: %s", e)
|
||||
self.active = True
|
||||
elif isinstance(event, StoppedPlaying):
|
||||
self.active = False
|
||||
original_brightness = self.light_state.get("state", {}).get(
|
||||
"bri"
|
||||
)
|
||||
if original_brightness is not None:
|
||||
try:
|
||||
hue_response(
|
||||
requests.put(
|
||||
f"http://{self.host}/api/{self.username}/lights/{self.light_id}/state",
|
||||
json={"bri": original_brightness},
|
||||
)
|
||||
)
|
||||
logger.info(
|
||||
"Restored %s to previous brightness", self.light
|
||||
)
|
||||
except HueError as e:
|
||||
logger.warn(f"Error restoring light brightness: %s", e)
|
||||
elif isinstance(event, Exit):
|
||||
stopping = True
|
||||
except queue.Empty:
|
||||
...
|
||||
if stopping:
|
||||
break
|
||||
try:
|
||||
while sample := self.pcm_in.get(False):
|
||||
audio = sample
|
||||
except queue.Empty:
|
||||
...
|
||||
if not audio:
|
||||
continue
|
||||
rms = audioop.rms(audio.raw, audio.channels)
|
||||
peak = audioop.max(audio.raw, audio.channels)
|
||||
max_peak = max(peak, max_peak)
|
||||
brightness = int(peak / max_peak * 255)
|
||||
logger.debug(f"Brightness: {brightness}")
|
||||
if audio and self.active:
|
||||
rms = audioop.rms(audio.raw, audio.channels)
|
||||
peak = audioop.max(audio.raw, audio.channels)
|
||||
max_peak = max(peak, max_peak)
|
||||
brightness = int(peak / max_peak * 255)
|
||||
logger.debug(f"Brightness: {brightness}")
|
||||
|
||||
requests.put(
|
||||
"http://192.168.1.199/api/bx1YKf6IQmU-W1MLHrsZ79Wz4bRWiBShb4ewBpfm/lights/7/state",
|
||||
json={"bri": brightness, "transitiontime": 1},
|
||||
)
|
||||
|
||||
# requests.put(
|
||||
# "http://192.168.1.199/api/bx1YKf6IQmU-W1MLHrsZ79Wz4bRWiBShb4ewBpfm/groups/2/action",
|
||||
# json={"bri": brightness, "transitiontime": 1},
|
||||
# )
|
||||
requests.put(
|
||||
f"http://{self.host}/api/{self.username}/lights/{self.light_id}/state",
|
||||
json={"bri": brightness, "transitiontime": 1},
|
||||
)
|
||||
|
||||
time.sleep(0.1)
|
||||
logger.info("Hue stopped")
|
||||
|
|
|
@ -52,3 +52,6 @@ class Icecast(Process):
|
|||
self.set_title("<Idle>")
|
||||
elif isinstance(event, NewMetadata):
|
||||
self.set_title(event.title)
|
||||
elif isinstance(event, Exit):
|
||||
break
|
||||
logger.info("Icecast Updater stopped")
|
||||
|
|
|
@ -4,6 +4,7 @@ import enum
|
|||
import logging
|
||||
from multiprocessing import Process, Queue
|
||||
from multiprocessing.connection import Connection
|
||||
import queue
|
||||
import struct
|
||||
import time
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple
|
||||
|
@ -53,6 +54,7 @@ class Turntable(Process):
|
|||
def __init__(
|
||||
self,
|
||||
pcm_in: "Queue[PCM]",
|
||||
events_in: "Queue[Event]",
|
||||
events_out: "List[Queue[Event]]",
|
||||
framerate: int,
|
||||
channels: int,
|
||||
|
@ -71,6 +73,7 @@ class Turntable(Process):
|
|||
self.buffer = PCM(framerate=framerate, channels=channels, maxlen=maxlen)
|
||||
self.recognizer = PCMRecognizer(dejavu)
|
||||
self.pcm_in = pcm_in
|
||||
self.events_in = events_in
|
||||
self.events_out = events_out
|
||||
self.state: State = State.idle
|
||||
self.identified = False
|
||||
|
@ -87,10 +90,20 @@ class Turntable(Process):
|
|||
|
||||
def run(self) -> None:
|
||||
logger.debug("Starting Turntable")
|
||||
while fragment := self.pcm_in.get():
|
||||
while True:
|
||||
try:
|
||||
event = self.events_in.get(block=False)
|
||||
if isinstance(event, Exit):
|
||||
self.publish(StoppedPlaying())
|
||||
self.publish(event)
|
||||
break
|
||||
except queue.Empty:
|
||||
...
|
||||
fragment = self.pcm_in.get()
|
||||
self.buffer.append(fragment)
|
||||
maximum = audioop.max(fragment.raw, 2)
|
||||
self.update_audiolevel(maximum)
|
||||
logger.info("Turntable stopped")
|
||||
|
||||
def publish(self, event: Event) -> None:
|
||||
for queue in self.events_out:
|
||||
|
|
Loading…
Reference in a new issue