diff --git a/poetry.lock b/poetry.lock index 1928815..0fc0287 100644 --- a/poetry.lock +++ b/poetry.lock @@ -280,7 +280,6 @@ scipy = "1.3.1" reference = "e56a4a221ad204654a191d217f92aebf3f058b62" type = "git" url = "https://github.com/worldveil/dejavu.git" - [[package]] category = "main" description = "Manipulate audio with an simple and easy high level interface" @@ -291,11 +290,11 @@ version = "0.23.1" [[package]] category = "main" -description = "Cross-platform windowing and multimedia library" -name = "pyglet" +description = "Python Game Development" +name = "pygame" optional = false python-versions = "*" -version = "1.5.7" +version = "1.9.6" [[package]] category = "main" @@ -422,7 +421,7 @@ secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "pyOpenSSL (>=0 socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] [metadata] -content-hash = "3af542607f4ccaa3afe08d275174f664cdacedac31ce8278e059d4aca2b9edeb" +content-hash = "9fa7d79cd4464df797b8d866bfa3a426ab1399854faf48b02f00d65d46a507be" lock-version = "1.0" python-versions = "^3.8" @@ -634,9 +633,33 @@ pydub = [ {file = "pydub-0.23.1-py2.py3-none-any.whl", hash = "sha256:d29901a486fb421c5d7b0f3d5d3a60527179204d8ffb20e74e1ae81c17e81b46"}, {file = "pydub-0.23.1.tar.gz", hash = "sha256:c362fa02da1eebd1d08bd47aa9b0102582dff7ca2269dbe9e043d228a0c1ea93"}, ] -pyglet = [ - {file = "pyglet-1.5.7-py3-none-any.whl", hash = "sha256:9832442d59ee06acbeff12e128cf6d5aee271e94c09386040db8f0feae277013"}, - {file = "pyglet-1.5.7.zip", hash = "sha256:3faac2dad34946aecbce79a8658f89155436fe5c07332229160c6eba302ff40d"}, +pygame = [ + {file = "pygame-1.9.6-cp27-cp27m-macosx_10_11_intel.whl", hash = "sha256:4aaff572a273a32e70ec3593d213e59ab11c183a9916616562247930f17a5447"}, + {file = "pygame-1.9.6-cp27-cp27m-win32.whl", hash = "sha256:73cd9df328c7e72638dbcc1d18e7155225faed880a53db6bad90d1d7c0a71dfd"}, + {file = "pygame-1.9.6-cp27-cp27m-win_amd64.whl", hash = "sha256:9ce22fb72298ea33dbb3a1c6c60a4a4e19d9698df6f3f5782eba4dada7b7736d"}, + {file = "pygame-1.9.6-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:5f052dc2975a399aa1830c1f04c5f72856aa416bf3cd4b31375a058015a5c620"}, + {file = "pygame-1.9.6-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:0480fe82cd41a43e3eea497fa2c059c72ac54cb5d003d5aa2ed06a04541c384e"}, + {file = "pygame-1.9.6-cp34-cp34m-manylinux1_i686.whl", hash = "sha256:a6e8d2f99dbe1dfe72d0c019693c14d93c410f702d0b04ec9a81b36dacd55a23"}, + {file = "pygame-1.9.6-cp34-cp34m-manylinux1_x86_64.whl", hash = "sha256:698433a9fcefca0527244dc44dff9503eb26157494730b1cc80e6e4dbb246e92"}, + {file = "pygame-1.9.6-cp34-cp34m-win32.whl", hash = "sha256:68ea43e51150316b9fb08e251209d4e2b4e76a340b5b6fc8cdf1a898c78f7e5b"}, + {file = "pygame-1.9.6-cp34-cp34m-win_amd64.whl", hash = "sha256:4e1065577f1b29111113be5deb2ea88553551a5e1cf33e0c08fa32768f285809"}, + {file = "pygame-1.9.6-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:6f714986f7987f10cb94f1be0753318e341a7ea6b12d66f37a4d5d6dd4695023"}, + {file = "pygame-1.9.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:ae1bc3e78ed28f20878e7ca2c98663a6634e9c00d7746d39413fc18e907dc162"}, + {file = "pygame-1.9.6-cp35-cp35m-win32.whl", hash = "sha256:854e87b8b2b76e3ed11d64985fcfdd7af919659503de99fc2b0a717b314c3cf0"}, + {file = "pygame-1.9.6-cp35-cp35m-win_amd64.whl", hash = "sha256:2622b9dd95f445c887a36a57eade42c672598589f69a8052ccdb8eeeffa4dbb1"}, + {file = "pygame-1.9.6-cp36-cp36m-macosx_10_11_intel.whl", hash = "sha256:398c42b605ecc514e62f68f1944a2d21e247938309f598de6cb0ad3c207324a8"}, + {file = "pygame-1.9.6-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:fa788f775680fc5d268ab00a2da29c9a22830032cfab732730298a2952cd87f3"}, + {file = "pygame-1.9.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:c895cf9c1b6d1cbba8cb8cc3f5427febcf8aa41a9333697741abeea1c537a350"}, + {file = "pygame-1.9.6-cp36-cp36m-win32.whl", hash = "sha256:a37b6c59e7b8feadc51db5197052b86ceb6443f9fb2a6f7d6527620e707c558c"}, + {file = "pygame-1.9.6-cp36-cp36m-win_amd64.whl", hash = "sha256:be7e70f91bd4eb35ae081062f16bf434619b3292358d9b061f8159ddc570c7f0"}, + {file = "pygame-1.9.6-cp37-cp37m-macosx_10_11_intel.whl", hash = "sha256:7876d1f29f66d3d7cac46479503891ee1ef409b0fbce54b0d74f3a6b33a46dba"}, + {file = "pygame-1.9.6-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:f1f5714d2c23f6a64ef2ac4fcd36a2dd2689da85978d951a99a6ae5dfdf9bdbc"}, + {file = "pygame-1.9.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:136a3b5711d9ec369a0407e4e08ffced3ba61aa41059e9280ffffa79c8614f65"}, + {file = "pygame-1.9.6-cp37-cp37m-win32.whl", hash = "sha256:a9ac862dd7159861f2c6443b0029089e1c0c4ec762a8074022914ec52fe4dfac"}, + {file = "pygame-1.9.6-cp37-cp37m-win_amd64.whl", hash = "sha256:8da13704ad45b7d5de8a8cca135a7f44c7fc6aa9f691abe7b0392468a34a8013"}, + {file = "pygame-1.9.6-cp38-cp38-win32.whl", hash = "sha256:396320aa29a925feed0b64639f77ce1418722ea7f536b4e4936083dd8d4c4535"}, + {file = "pygame-1.9.6-cp38-cp38-win_amd64.whl", hash = "sha256:e3e7e4a09dfd8b03663222d6bcadec9fef021404f4d9eecf56825342e039dfc1"}, + {file = "pygame-1.9.6.tar.gz", hash = "sha256:301c6428c0880ecd4a9e3951b80e539c33863b6ff356a443db1758de4f297957"}, ] pyparsing = [ {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, diff --git a/pyproject.toml b/pyproject.toml index e796fc5..e2637a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ python = "^3.8" pyalsaaudio = "^0.9.0" pydejavu = {git = "https://github.com/worldveil/dejavu.git"} requests = "^2.24.0" -pyglet = "^1.5.7" +pygame = "^1.9.6" [tool.poetry.dev-dependencies] black = "^20.8b1" diff --git a/turntable/application.py b/turntable/application.py index 43a9bdb..7d3cac5 100644 --- a/turntable/application.py +++ b/turntable/application.py @@ -23,63 +23,69 @@ from turntable.turntable import ( VERSION = importlib.metadata.version("turntable") logger = logging.getLogger(__name__) -@contextmanager -def run() -> "Iterator[Queue[Event]]": - parser = argparse.ArgumentParser() - parser.add_argument( - "--config", default=os.path.expanduser("~/.config/turntable.json") - ) - args = parser.parse_args() - with open(args.config, "r") as config_file: - config: Dict[str, Any] = json.load(config_file) - logging.basicConfig(level=logging.DEBUG if config.get("debug") else logging.INFO) - logger.info("Turntable version %s", VERSION) +class Application: + def __init__(self): + parser = argparse.ArgumentParser() + parser.add_argument( + "--config", default=os.path.expanduser("~/.config/turntable.json") + ) + args = parser.parse_args() + with open(args.config, "r") as config_file: + self.config: Dict[str, Any] = json.load(config_file) + logging.basicConfig( + level=logging.DEBUG if self.config.get("debug") else logging.INFO + ) + logger.info("Turntable version %s", VERSION) - pcm_in: "Queue[PCM]" = Queue() - pcm_out: "Queue[PCM]" = Queue() - events: "Queue[Event]" = Queue() + pcm_in: "Queue[PCM]" = Queue() + pcm_out: "Queue[PCM]" = Queue() + self.pcm_display: "Queue[PCM]" = Queue() + self.events: "Queue[Event]" = Queue() - audio_config = config.get("audio", dict()) - listener = Listener( - [pcm_in, pcm_out], - 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), - ) + audio_config = self.config.get("audio", dict()) + listener = Listener( + [pcm_in, pcm_out, self.pcm_display], + self.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), + ) - 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), - ) + 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), + ) - dejavu = Dejavu(config.get("dejavu", dict())) + dejavu = Dejavu(self.config.get("dejavu", dict())) - turntable = Turntable(listener.framerate, listener.channels, dejavu, pcm_in, events) + turntable = Turntable( + listener.framerate, listener.channels, dejavu, pcm_in, self.events + ) - icecast_config = 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"), - ) + 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"), + ) - processes = [listener, player, turntable] - for process in processes: - process.daemon = True - process.start() - try: - yield events - except: - logging.exception("Terminating") - for process in processes: + self.processes = [listener, player, turntable] + + def run(self) -> "Iterator[Queue[Event]]": + for process in self.processes: + process.daemon = True + process.start() + + def shutdown(self) -> None: + logging.info("Terminating") + for process in self.processes: if process.is_alive(): - process.terminate() + process.kill() diff --git a/turntable/cli.py b/turntable/cli.py index f44b7ef..43184f5 100644 --- a/turntable/cli.py +++ b/turntable/cli.py @@ -4,7 +4,8 @@ from turntable import application, turntable def main() -> None: - with application.run() as events: + app = application.Application() + with app.run() as events: while event := events.get(): if not isinstance(event, turntable.Audio): logging.info("Event: %s", event) diff --git a/turntable/gui.py b/turntable/gui.py index 198f638..6a8a917 100644 --- a/turntable/gui.py +++ b/turntable/gui.py @@ -1,19 +1,23 @@ import logging +import os import queue from statistics import fmean from typing import Iterable, List, Optional, Tuple, Union import numpy as np # type: ignore -import pyglet # type: ignore -import pyglet.clock # type: ignore +import pygame +from pygame.locals import * import scipy.signal # type: ignore from turntable import application, models, turntable +logger = logging.getLogger(__name__) + class Plot: def __init__( self, + screen, x: int, y: int, width: int, @@ -21,8 +25,8 @@ class Plot: bars: int = 20, bar_width: int = 40, color: Tuple[int, int, int] = (255, 255, 255), - batch: Optional[pyglet.graphics.Batch] = None, ) -> None: + self.screen = screen self.x = x self.y = y self.width = width @@ -30,80 +34,98 @@ class Plot: self.bars = bars self.bar_width = bar_width self.color = color - self.batch = batch or pyglet.graphics.Batch() - self.lines: List[pyglet.shapes.Line] = [] self.audio = b"" - def update(self): + def draw(self) -> None: data = np.fromstring(self.audio, dtype=np.int16) + if len(data) == 0: + return fft = abs(np.fft.fft(data).real) fft = fft[: len(fft) // 2] heights = scipy.signal.resample(fft, self.bars) * self.height / 2 ** 16 - self.lines = [ - pyglet.shapes.Line( - self.x + x / self.bars * self.width, + for i, height in enumerate(heights): + pygame.draw.rect( + self.screen, + self.color, + ( + self.x + i / self.bars * self.width, + self.height, + self.bar_width, + -height, + ), 0, - self.x + x / self.bars * self.width, - y, - width=self.bar_width, - color=self.color, - batch=self.batch, ) - for x, y in enumerate(heights) - ] - - def draw(self) -> None: - self.batch.draw() def main(): - window = pyglet.window.Window(fullscreen=True) - with application.run() as events: - audio = b"" - label = pyglet.text.Label( - "", - font_name="Noto Sans", - font_size=36, - x=window.width // 2, - y=window.height // 2, - anchor_x="center", - anchor_y="center", - ) - batch = pyglet.graphics.Batch() - plot = Plot( - x=0, - y=0, - width=window.width, - height=window.height, - bars=40, - bar_width=window.width // 45, - color=(139, 0, 139), - batch=batch, - ) + app = application.Application() + config = app.config.get("gui", dict()) + FPS = int(config.get("fps", 30)) + WIDTH = int(config.get("width", 800)) + HEIGHT = int(config.get("height", 600)) + disp_no = os.getenv("DISPLAY") + if disp_no: + logger.info("I'm running under X display = {0}".format(disp_no)) - @window.event - def on_draw(): - window.clear() - batch.draw() - label.draw() + # Check which frame buffer drivers are available + # Start with fbcon since directfb hangs with composite output + drivers = ["x11", "fbcon", "directfb", "svgalib"] + found = False + for driver in drivers: + # Make sure that SDL_VIDEODRIVER is set + if not os.getenv("SDL_VIDEODRIVER"): + os.putenv("SDL_VIDEODRIVER", driver) + try: + pygame.display.init() + except pygame.error: + logger.warn("Driver: {0} failed.".format(driver)) + continue + found = True + break - def check_events(dt): - try: - event = events.get(False) - if isinstance(event, turntable.StartedPlaying): - label.text = "" - elif isinstance(event, turntable.StoppedPlaying): - label.text = "" - elif isinstance(event, turntable.NewMetadata): - label.text = event.title - elif isinstance(event, turntable.Audio): - plot.audio = event.pcm.raw - except queue.Empty: + if not found: + raise Exception("No suitable video driver found!") + + size = (pygame.display.Info().current_w, pygame.display.Info().current_h) + logger.info("Window size: %d x %d" % (size[0], size[1])) + screen = pygame.display.set_mode((WIDTH, HEIGHT)) + # Clear the screen to start + screen.fill((0, 0, 0)) + # Initialise font support + pygame.font.init() + # Render the screen + pygame.display.update() + + plot = Plot( + screen=screen, + x=0, + y=0, + width=screen.get_width(), + height=screen.get_height(), + bars=40, + bar_width=screen.get_width() // 45, + 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): ... - - def update_vis(dt): - plot.update() - - pyglet.clock.schedule(check_events) - pyglet.clock.schedule_interval(update_vis, 0.03) - pyglet.app.run() + 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(30)