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 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()

View file

@ -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)

View file

@ -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()

View file

@ -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()

View file

@ -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("<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):
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())