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