mirror of
https://github.com/correl/turntable.git
synced 2024-12-27 11:09:53 +00:00
Rearrange processes and reintroduce icecast
This commit is contained in:
parent
5640790a28
commit
bb3b822cf6
6 changed files with 134 additions and 78 deletions
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Reference in a new issue