Rearrange processes and reintroduce icecast

This commit is contained in:
Correl Roush 2020-09-04 21:41:06 -04:00
parent 5640790a28
commit bb3b822cf6
6 changed files with 134 additions and 78 deletions

View file

@ -3,29 +3,24 @@ from contextlib import contextmanager
import importlib.metadata import importlib.metadata
import json import json
import logging import logging
from multiprocessing import Queue from multiprocessing import Process, Queue
import os import os
from typing import Any, Dict, Iterator 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.icecast import Icecast from turntable.icecast import Icecast
from turntable.models import PCM from turntable.models import PCM
from turntable.turntable import ( from turntable.turntable import Turntable
Event,
NewMetadata,
StartedPlaying,
StoppedPlaying,
Turntable,
)
VERSION = importlib.metadata.version("turntable") VERSION = importlib.metadata.version("turntable")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Application: class Application:
def __init__(self): def __init__(self, events: "Queue[Event]", pcm: "Optional[Queue[PCM]]" = None):
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")
@ -39,48 +34,64 @@ class Application:
) )
logger.info("Turntable version %s", VERSION) logger.info("Turntable version %s", VERSION)
pcm_in: "Queue[PCM]" = Queue() self.processes: List[Process] = []
pcm_out: "Queue[PCM]" = Queue() event_queues: "List[Queue[Event]]" = [events]
self.pcm_display: "Queue[PCM]" = Queue()
self.events: "Queue[Event]" = Queue()
audio_config = self.config.get("audio", dict()) 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( listener = Listener(
[pcm_in, pcm_out, self.pcm_display], pcms,
self.events, events,
audio_config.get("device", "default"), audio_config.get("device", "default"),
framerate=audio_config.get("framerate", 44100), framerate=audio_config.get("framerate", 44100),
channels=audio_config.get("channels", 2), channels=audio_config.get("channels", 2),
period_size=audio_config.get("period_size", 4096), period_size=audio_config.get("period_size", 4096),
) )
self.processes.append(listener)
player = Player( icecast_config = self.config.get("icecast", dict())
pcm_out, icecast_enabled = icecast_config.get("enabled", False)
audio_config.get("output_device", "null"), if icecast_enabled:
framerate=audio_config.get("framerate", 44100), icecast_events: "Queue[Event]" = Queue()
channels=audio_config.get("channels", 2), icecast = Icecast(
period_size=audio_config.get("period_size", 4096), 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())) dejavu = Dejavu(self.config.get("dejavu", dict()))
turntable = Turntable( 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()) def run(self) -> None:
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]]":
for process in self.processes: for process in self.processes:
logging.info("Starting %s", process)
process.daemon = True process.daemon = True
process.start() process.start()

View file

@ -47,7 +47,7 @@ class Listener(Process):
elif framerate not in range(*available_rates): elif framerate not in range(*available_rates):
raise ValueError(f"Unsupported framerate: {framerate}") raise ValueError(f"Unsupported framerate: {framerate}")
logger.info( logger.info(
"Listener started on '%s' [rate=%d, channels=%d, periodsize=%d]", "Listener ready on '%s' [rate=%d, channels=%d, periodsize=%d]",
device, device,
framerate, framerate,
channels, channels,
@ -55,6 +55,7 @@ class Listener(Process):
) )
def run(self) -> None: def run(self) -> None:
logger.debug("Starting Listener")
framecount = 0 framecount = 0
event_limit = self.framerate event_limit = self.framerate
while True: while True:
@ -96,7 +97,7 @@ class Player(Process):
channels=channels, channels=channels,
) )
logger.info( logger.info(
"Player started on '%s' [rate=%d, channels=%d, periodsize=%d]", "Player ready on '%s' [rate=%d, channels=%d, periodsize=%d]",
device, device,
framerate, framerate,
channels, channels,
@ -104,5 +105,6 @@ class Player(Process):
) )
def run(self) -> None: def run(self) -> None:
logger.debug("Starting Player")
while pcm := self.pcm_in.get(): while pcm := self.pcm_in.get():
self.playback.write(pcm.raw) self.playback.write(pcm.raw)

View file

@ -1,11 +1,16 @@
import logging import logging
from multiprocessing import Queue
from turntable import application, turntable from turntable.application import Application
from turntable.events import Event
def main() -> None: def main() -> None:
app = application.Application() events: "Queue[Event]" = Queue()
with app.run() as events: app = Application(events)
app.run()
try:
while event := events.get(): while event := events.get():
if not isinstance(event, turntable.Audio): logging.info("Event: %s", event)
logging.info("Event: %s", event) except:
app.shutdown()

View file

@ -1,15 +1,16 @@
import logging import logging
from multiprocessing import Queue
import os import os
import queue import queue
from statistics import fmean from statistics import fmean
from typing import Iterable, List, Optional, Tuple, Union from typing import Iterable, List, Optional, Tuple, Union
import numpy as np # type: ignore import numpy as np # type: ignore
import pygame import pygame # type: ignore
from pygame.locals import * from pygame.locals import * # type: ignore
import scipy.signal # type: ignore import scipy.signal # type: ignore
from turntable import application, models, turntable from turntable import application, events, models, turntable
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -58,7 +59,9 @@ class Plot:
def main(): 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()) config = app.config.get("gui", dict())
disp_no = os.getenv("DISPLAY") disp_no = os.getenv("DISPLAY")
if disp_no: if disp_no:
@ -108,25 +111,28 @@ def main():
color=(139, 0, 139), color=(139, 0, 139),
) )
app.run() try:
clock = pygame.time.Clock() app.run()
while True: clock = pygame.time.Clock()
for event in pygame.event.get(): while True:
if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE): for event in pygame.event.get():
app.shutdown() if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
pygame.quit() app.shutdown()
return pygame.quit()
try: return
while event := app.events.get(False): try:
while event := events.get(False):
...
except queue.Empty:
... ...
except queue.Empty: try:
... while sample := pcm_in.get(False):
try: plot.audio = sample.raw
while pcm := app.pcm_display.get(False): except queue.Empty:
plot.audio = pcm.raw ...
except queue.Empty: screen.fill((0, 0, 0))
... plot.draw()
screen.fill((0, 0, 0)) pygame.display.update()
plot.draw() clock.tick(FPS)
pygame.display.update() except:
clock.tick(FPS) app.shutdown()

View file

@ -1,19 +1,35 @@
import logging import logging
from multiprocessing import Process, Queue
import os import os
import requests import requests
from turntable.events import *
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Icecast: class Icecast(Process):
def __init__(self, host: str, port: int, mountpoint: str, user: str, password: str): 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.host = host
self.port = port self.port = port
self.mountpoint = mountpoint self.mountpoint = mountpoint
self.credentials = (user, password) 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: try:
requests.get( requests.get(
f"http://{self.host}:{self.port}/admin/metadata", f"http://{self.host}:{self.port}/admin/metadata",
@ -26,3 +42,14 @@ class Icecast:
) )
except requests.RequestException as e: except requests.RequestException as e:
logger.warning("Failed to update icecast metadata: %s", e) logger.warning("Failed to update icecast metadata: %s", e)
def run(self) -> None:
logger.debug("Starting Icecast Updater")
self.set_title("<Idle>")
while event := self.events.get():
if isinstance(event, StartedPlaying):
self.set_title("<Starting...>")
elif isinstance(event, StoppedPlaying):
self.set_title("<Idle>")
elif isinstance(event, NewMetadata):
self.set_title(event.title)

View file

@ -61,11 +61,11 @@ class PCMRecognizer(BaseRecognizer):
class Turntable(Process): class Turntable(Process):
def __init__( def __init__(
self, self,
pcm_in: "Queue[PCM]",
events_out: "List[Queue[Event]]",
framerate: int, framerate: int,
channels: int, channels: int,
dejavu: Dejavu, dejavu: Dejavu,
pcm_in: "Queue[PCM]",
events_out: "Queue[Event]",
) -> None: ) -> None:
super().__init__() super().__init__()
maxlen = channels * 2 * framerate * SAMPLE_SECONDS maxlen = channels * 2 * framerate * SAMPLE_SECONDS
@ -77,14 +77,19 @@ class Turntable(Process):
self.identified = False self.identified = False
self.captured = False self.captured = False
self.last_update: float = time.time() self.last_update: float = time.time()
logger.info("Turntable ready")
def run(self) -> None: def run(self) -> None:
logger.info("Initializing Turntable") logger.debug("Starting Turntable")
while fragment := self.pcm_in.get(): while 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)
def publish(self, event: Event) -> None:
for queue in self.events_out:
queue.put(event)
def update_audiolevel(self, level: int) -> None: def update_audiolevel(self, level: int) -> None:
newstate = self.state newstate = self.state
now = time.time() now = time.time()
@ -106,13 +111,13 @@ class Turntable(Process):
identification = self.recognizer.recognize(sample) identification = self.recognizer.recognize(sample)
logger.debug("Dejavu results: %s", identification) logger.debug("Dejavu results: %s", identification)
if results := identification[dejavu.config.settings.RESULTS]: if results := identification[dejavu.config.settings.RESULTS]:
self.events_out.put( self.publish(
NewMetadata( NewMetadata(
results[0][dejavu.config.settings.SONG_NAME].decode("utf-8") results[0][dejavu.config.settings.SONG_NAME].decode("utf-8")
) )
) )
else: else:
self.events_out.put(NewMetadata("Unknown Artist - Unknown Album")) self.publish(NewMetadata("Unknown Artist - Unknown Album"))
self.identified = True self.identified = True
elif ( elif (
now - self.last_update >= FINGERPRINT_DELAY + FINGERPRINT_STORE_SECONDS now - self.last_update >= FINGERPRINT_DELAY + FINGERPRINT_STORE_SECONDS
@ -143,8 +148,8 @@ class Turntable(Process):
self.last_update = updated_at self.last_update = updated_at
if to_state == State.idle: if to_state == State.idle:
self.events_out.put(StoppedPlaying()) self.publish(StoppedPlaying())
self.identified = False self.identified = False
self.captured = False self.captured = False
elif from_state == State.idle and to_state == State.playing: elif from_state == State.idle and to_state == State.playing:
self.events_out.put(StartedPlaying()) self.publish(StartedPlaying())