From bb3b822cf66d627a7c07a158c63e1826deabd0b4 Mon Sep 17 00:00:00 2001 From: Correl Roush Date: Fri, 4 Sep 2020 21:41:06 -0400 Subject: [PATCH] Rearrange processes and reintroduce icecast --- turntable/application.py | 83 +++++++++++++++++++++++----------------- turntable/audio.py | 6 ++- turntable/cli.py | 15 +++++--- turntable/gui.py | 56 +++++++++++++++------------ turntable/icecast.py | 33 ++++++++++++++-- turntable/turntable.py | 19 +++++---- 6 files changed, 134 insertions(+), 78 deletions(-) diff --git a/turntable/application.py b/turntable/application.py index 7d3cac5..8d13249 100644 --- a/turntable/application.py +++ b/turntable/application.py @@ -3,29 +3,24 @@ from contextlib import contextmanager import importlib.metadata import json import logging -from multiprocessing import Queue +from multiprocessing import Process, Queue import os -from typing import Any, Dict, Iterator +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.icecast import Icecast from turntable.models import PCM -from turntable.turntable import ( - Event, - NewMetadata, - StartedPlaying, - StoppedPlaying, - Turntable, -) +from turntable.turntable import Turntable VERSION = importlib.metadata.version("turntable") logger = logging.getLogger(__name__) class Application: - def __init__(self): + def __init__(self, events: "Queue[Event]", pcm: "Optional[Queue[PCM]]" = None): parser = argparse.ArgumentParser() parser.add_argument( "--config", default=os.path.expanduser("~/.config/turntable.json") @@ -39,48 +34,64 @@ class Application: ) logger.info("Turntable version %s", VERSION) - pcm_in: "Queue[PCM]" = Queue() - pcm_out: "Queue[PCM]" = Queue() - self.pcm_display: "Queue[PCM]" = Queue() - self.events: "Queue[Event]" = Queue() + self.processes: List[Process] = [] + event_queues: "List[Queue[Event]]" = [events] audio_config = self.config.get("audio", dict()) + pcm_in: "Queue[PCM]" = Queue() + pcms: "List[Queue[PCM]]" = [pcm_in] + if pcm: + pcms.append(pcm) + if output_device := audio_config.get("output_device"): + pcm_out: "Queue[PCM]" = Queue() + player = Player( + pcm_out, + audio_config.get("output_device", "null"), + framerate=audio_config.get("framerate", 44100), + channels=audio_config.get("channels", 2), + period_size=audio_config.get("period_size", 4096), + ) + self.processes.append(player) + pcms.append(pcm_out) listener = Listener( - [pcm_in, pcm_out, self.pcm_display], - self.events, + pcms, + events, audio_config.get("device", "default"), framerate=audio_config.get("framerate", 44100), channels=audio_config.get("channels", 2), period_size=audio_config.get("period_size", 4096), ) + self.processes.append(listener) - player = Player( - pcm_out, - audio_config.get("output_device", "null"), - framerate=audio_config.get("framerate", 44100), - channels=audio_config.get("channels", 2), - period_size=audio_config.get("period_size", 4096), - ) + icecast_config = self.config.get("icecast", dict()) + icecast_enabled = icecast_config.get("enabled", False) + if icecast_enabled: + icecast_events: "Queue[Event]" = Queue() + icecast = Icecast( + events=icecast_events, + host=icecast_config.get("host", "localhost"), + port=icecast_config.get("port", 8000), + mountpoint=icecast_config.get("mountpoint", "stream.mp3"), + user=icecast_config.get("admin_user", "admin"), + password=icecast_config.get("admin_password", "hackme"), + ) + event_queues.append(icecast_events) + self.processes.append(icecast) dejavu = Dejavu(self.config.get("dejavu", dict())) turntable = Turntable( - listener.framerate, listener.channels, dejavu, pcm_in, self.events + pcm_in, + event_queues, + listener.framerate, + listener.channels, + dejavu, ) + self.processes.append(turntable) - icecast_config = self.config.get("icecast", dict()) - icecast = Icecast( - host=icecast_config.get("host", "localhost"), - port=icecast_config.get("port", 8000), - mountpoint=icecast_config.get("mountpoint", "stream.mp3"), - user=icecast_config.get("admin_user", "admin"), - password=icecast_config.get("admin_password", "hackme"), - ) - - self.processes = [listener, player, turntable] - - def run(self) -> "Iterator[Queue[Event]]": + def run(self) -> None: for process in self.processes: + logging.info("Starting %s", process) process.daemon = True process.start() diff --git a/turntable/audio.py b/turntable/audio.py index 317c1c8..aa5b8fb 100644 --- a/turntable/audio.py +++ b/turntable/audio.py @@ -47,7 +47,7 @@ class Listener(Process): elif framerate not in range(*available_rates): raise ValueError(f"Unsupported framerate: {framerate}") logger.info( - "Listener started on '%s' [rate=%d, channels=%d, periodsize=%d]", + "Listener ready on '%s' [rate=%d, channels=%d, periodsize=%d]", device, framerate, channels, @@ -55,6 +55,7 @@ class Listener(Process): ) def run(self) -> None: + logger.debug("Starting Listener") framecount = 0 event_limit = self.framerate while True: @@ -96,7 +97,7 @@ class Player(Process): channels=channels, ) logger.info( - "Player started on '%s' [rate=%d, channels=%d, periodsize=%d]", + "Player ready on '%s' [rate=%d, channels=%d, periodsize=%d]", device, framerate, channels, @@ -104,5 +105,6 @@ class Player(Process): ) def run(self) -> None: + logger.debug("Starting Player") while pcm := self.pcm_in.get(): self.playback.write(pcm.raw) diff --git a/turntable/cli.py b/turntable/cli.py index 43184f5..1b06727 100644 --- a/turntable/cli.py +++ b/turntable/cli.py @@ -1,11 +1,16 @@ import logging +from multiprocessing import Queue -from turntable import application, turntable +from turntable.application import Application +from turntable.events import Event def main() -> None: - app = application.Application() - with app.run() as events: + events: "Queue[Event]" = Queue() + app = Application(events) + app.run() + try: while event := events.get(): - if not isinstance(event, turntable.Audio): - logging.info("Event: %s", event) + logging.info("Event: %s", event) + except: + app.shutdown() diff --git a/turntable/gui.py b/turntable/gui.py index 861741f..c028fe8 100644 --- a/turntable/gui.py +++ b/turntable/gui.py @@ -1,15 +1,16 @@ import logging +from multiprocessing import Queue import os import queue from statistics import fmean from typing import Iterable, List, Optional, Tuple, Union import numpy as np # type: ignore -import pygame -from pygame.locals import * +import pygame # type: ignore +from pygame.locals import * # type: ignore import scipy.signal # type: ignore -from turntable import application, models, turntable +from turntable import application, events, models, turntable logger = logging.getLogger(__name__) @@ -58,7 +59,9 @@ class Plot: def main(): - app = application.Application() + events: "Queue[events.Event]" = Queue() + pcm_in: "Queue[models.PCM]" = Queue() + app = application.Application(events, pcm_in) config = app.config.get("gui", dict()) disp_no = os.getenv("DISPLAY") if disp_no: @@ -108,25 +111,28 @@ def main(): color=(139, 0, 139), ) - app.run() - clock = pygame.time.Clock() - while True: - for event in pygame.event.get(): - if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): - app.shutdown() - pygame.quit() - return - try: - while event := app.events.get(False): + try: + app.run() + clock = pygame.time.Clock() + while True: + for event in pygame.event.get(): + if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): + app.shutdown() + pygame.quit() + return + try: + while event := events.get(False): + ... + except queue.Empty: ... - except queue.Empty: - ... - try: - while pcm := app.pcm_display.get(False): - plot.audio = pcm.raw - except queue.Empty: - ... - screen.fill((0, 0, 0)) - plot.draw() - pygame.display.update() - clock.tick(FPS) + try: + while sample := pcm_in.get(False): + plot.audio = sample.raw + except queue.Empty: + ... + screen.fill((0, 0, 0)) + plot.draw() + pygame.display.update() + clock.tick(FPS) + except: + app.shutdown() diff --git a/turntable/icecast.py b/turntable/icecast.py index 203dc64..07eee03 100644 --- a/turntable/icecast.py +++ b/turntable/icecast.py @@ -1,19 +1,35 @@ import logging +from multiprocessing import Process, Queue import os import requests +from turntable.events import * + logger = logging.getLogger(__name__) -class Icecast: - def __init__(self, host: str, port: int, mountpoint: str, user: str, password: str): +class Icecast(Process): + def __init__( + self, + events: "Queue[Event]", + host: str, + port: int, + mountpoint: str, + user: str, + password: str, + ) -> None: + super().__init__() + self.events = events self.host = host self.port = port self.mountpoint = mountpoint self.credentials = (user, password) + logger.info("Icecast Updater ready for '%s:%d/%s'", host, port, mountpoint) - def set_title(self, title: str): + def set_title(self, title: str) -> None: + return + logging.info("Updating icecast title to '%s'", title) try: requests.get( f"http://{self.host}:{self.port}/admin/metadata", @@ -26,3 +42,14 @@ class Icecast: ) except requests.RequestException as e: logger.warning("Failed to update icecast metadata: %s", e) + + def run(self) -> None: + logger.debug("Starting Icecast Updater") + self.set_title("") + while event := self.events.get(): + if isinstance(event, StartedPlaying): + self.set_title("") + elif isinstance(event, StoppedPlaying): + self.set_title("") + elif isinstance(event, NewMetadata): + self.set_title(event.title) diff --git a/turntable/turntable.py b/turntable/turntable.py index 9a5f5f9..bc78a79 100644 --- a/turntable/turntable.py +++ b/turntable/turntable.py @@ -61,11 +61,11 @@ class PCMRecognizer(BaseRecognizer): class Turntable(Process): def __init__( self, + pcm_in: "Queue[PCM]", + events_out: "List[Queue[Event]]", framerate: int, channels: int, dejavu: Dejavu, - pcm_in: "Queue[PCM]", - events_out: "Queue[Event]", ) -> None: super().__init__() maxlen = channels * 2 * framerate * SAMPLE_SECONDS @@ -77,14 +77,19 @@ class Turntable(Process): self.identified = False self.captured = False self.last_update: float = time.time() + logger.info("Turntable ready") def run(self) -> None: - logger.info("Initializing Turntable") + logger.debug("Starting Turntable") while fragment := self.pcm_in.get(): self.buffer.append(fragment) maximum = audioop.max(fragment.raw, 2) self.update_audiolevel(maximum) + def publish(self, event: Event) -> None: + for queue in self.events_out: + queue.put(event) + def update_audiolevel(self, level: int) -> None: newstate = self.state now = time.time() @@ -106,13 +111,13 @@ class Turntable(Process): identification = self.recognizer.recognize(sample) logger.debug("Dejavu results: %s", identification) if results := identification[dejavu.config.settings.RESULTS]: - self.events_out.put( + self.publish( NewMetadata( results[0][dejavu.config.settings.SONG_NAME].decode("utf-8") ) ) else: - self.events_out.put(NewMetadata("Unknown Artist - Unknown Album")) + self.publish(NewMetadata("Unknown Artist - Unknown Album")) self.identified = True elif ( now - self.last_update >= FINGERPRINT_DELAY + FINGERPRINT_STORE_SECONDS @@ -143,8 +148,8 @@ class Turntable(Process): self.last_update = updated_at if to_state == State.idle: - self.events_out.put(StoppedPlaying()) + self.publish(StoppedPlaying()) self.identified = False self.captured = False elif from_state == State.idle and to_state == State.playing: - self.events_out.put(StartedPlaying()) + self.publish(StartedPlaying())