mirror of
https://github.com/correl/turntable.git
synced 2024-11-23 11:09:56 +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 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()
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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())
|
||||||
|
|
Loading…
Reference in a new issue