Replace pyglet with pygame

This commit is contained in:
Correl Roush 2020-09-04 20:05:18 -04:00
parent 08834afe21
commit a4a42bd632
5 changed files with 178 additions and 126 deletions

39
poetry.lock generated
View file

@ -280,7 +280,6 @@ scipy = "1.3.1"
reference = "e56a4a221ad204654a191d217f92aebf3f058b62" reference = "e56a4a221ad204654a191d217f92aebf3f058b62"
type = "git" type = "git"
url = "https://github.com/worldveil/dejavu.git" url = "https://github.com/worldveil/dejavu.git"
[[package]] [[package]]
category = "main" category = "main"
description = "Manipulate audio with an simple and easy high level interface" description = "Manipulate audio with an simple and easy high level interface"
@ -291,11 +290,11 @@ version = "0.23.1"
[[package]] [[package]]
category = "main" category = "main"
description = "Cross-platform windowing and multimedia library" description = "Python Game Development"
name = "pyglet" name = "pygame"
optional = false optional = false
python-versions = "*" python-versions = "*"
version = "1.5.7" version = "1.9.6"
[[package]] [[package]]
category = "main" 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)"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"]
[metadata] [metadata]
content-hash = "3af542607f4ccaa3afe08d275174f664cdacedac31ce8278e059d4aca2b9edeb" content-hash = "9fa7d79cd4464df797b8d866bfa3a426ab1399854faf48b02f00d65d46a507be"
lock-version = "1.0" lock-version = "1.0"
python-versions = "^3.8" 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-py2.py3-none-any.whl", hash = "sha256:d29901a486fb421c5d7b0f3d5d3a60527179204d8ffb20e74e1ae81c17e81b46"},
{file = "pydub-0.23.1.tar.gz", hash = "sha256:c362fa02da1eebd1d08bd47aa9b0102582dff7ca2269dbe9e043d228a0c1ea93"}, {file = "pydub-0.23.1.tar.gz", hash = "sha256:c362fa02da1eebd1d08bd47aa9b0102582dff7ca2269dbe9e043d228a0c1ea93"},
] ]
pyglet = [ pygame = [
{file = "pyglet-1.5.7-py3-none-any.whl", hash = "sha256:9832442d59ee06acbeff12e128cf6d5aee271e94c09386040db8f0feae277013"}, {file = "pygame-1.9.6-cp27-cp27m-macosx_10_11_intel.whl", hash = "sha256:4aaff572a273a32e70ec3593d213e59ab11c183a9916616562247930f17a5447"},
{file = "pyglet-1.5.7.zip", hash = "sha256:3faac2dad34946aecbce79a8658f89155436fe5c07332229160c6eba302ff40d"}, {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 = [ pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},

View file

@ -10,7 +10,7 @@ python = "^3.8"
pyalsaaudio = "^0.9.0" pyalsaaudio = "^0.9.0"
pydejavu = {git = "https://github.com/worldveil/dejavu.git"} pydejavu = {git = "https://github.com/worldveil/dejavu.git"}
requests = "^2.24.0" requests = "^2.24.0"
pyglet = "^1.5.7" pygame = "^1.9.6"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
black = "^20.8b1" black = "^20.8b1"

View file

@ -23,28 +23,31 @@ from turntable.turntable import (
VERSION = importlib.metadata.version("turntable") VERSION = importlib.metadata.version("turntable")
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@contextmanager
def run() -> "Iterator[Queue[Event]]": class Application:
def __init__(self):
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")
) )
args = parser.parse_args() args = parser.parse_args()
with open(args.config, "r") as config_file: with open(args.config, "r") as config_file:
config: Dict[str, Any] = json.load(config_file) self.config: Dict[str, Any] = json.load(config_file)
logging.basicConfig(level=logging.DEBUG if config.get("debug") else logging.INFO) logging.basicConfig(
level=logging.DEBUG if self.config.get("debug") else logging.INFO
)
logger.info("Turntable version %s", VERSION) logger.info("Turntable version %s", VERSION)
pcm_in: "Queue[PCM]" = Queue() pcm_in: "Queue[PCM]" = Queue()
pcm_out: "Queue[PCM]" = Queue() pcm_out: "Queue[PCM]" = Queue()
events: "Queue[Event]" = Queue() self.pcm_display: "Queue[PCM]" = Queue()
self.events: "Queue[Event]" = Queue()
audio_config = config.get("audio", dict()) audio_config = self.config.get("audio", dict())
listener = Listener( listener = Listener(
[pcm_in, pcm_out], [pcm_in, pcm_out, self.pcm_display],
events, self.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),
@ -59,11 +62,13 @@ def run() -> "Iterator[Queue[Event]]":
period_size=audio_config.get("period_size", 4096), 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_config = self.config.get("icecast", dict())
icecast = Icecast( icecast = Icecast(
host=icecast_config.get("host", "localhost"), host=icecast_config.get("host", "localhost"),
port=icecast_config.get("port", 8000), port=icecast_config.get("port", 8000),
@ -72,14 +77,15 @@ def run() -> "Iterator[Queue[Event]]":
password=icecast_config.get("admin_password", "hackme"), password=icecast_config.get("admin_password", "hackme"),
) )
processes = [listener, player, turntable] self.processes = [listener, player, turntable]
for process in processes:
def run(self) -> "Iterator[Queue[Event]]":
for process in self.processes:
process.daemon = True process.daemon = True
process.start() process.start()
try:
yield events def shutdown(self) -> None:
except: logging.info("Terminating")
logging.exception("Terminating") for process in self.processes:
for process in processes:
if process.is_alive(): if process.is_alive():
process.terminate() process.kill()

View file

@ -4,7 +4,8 @@ from turntable import application, turntable
def main() -> None: def main() -> None:
with application.run() as events: app = application.Application()
with app.run() as events:
while event := events.get(): while event := events.get():
if not isinstance(event, turntable.Audio): if not isinstance(event, turntable.Audio):
logging.info("Event: %s", event) logging.info("Event: %s", event)

View file

@ -1,19 +1,23 @@
import logging import logging
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 pyglet # type: ignore import pygame
import pyglet.clock # type: ignore from pygame.locals import *
import scipy.signal # type: ignore import scipy.signal # type: ignore
from turntable import application, models, turntable from turntable import application, models, turntable
logger = logging.getLogger(__name__)
class Plot: class Plot:
def __init__( def __init__(
self, self,
screen,
x: int, x: int,
y: int, y: int,
width: int, width: int,
@ -21,8 +25,8 @@ class Plot:
bars: int = 20, bars: int = 20,
bar_width: int = 40, bar_width: int = 40,
color: Tuple[int, int, int] = (255, 255, 255), color: Tuple[int, int, int] = (255, 255, 255),
batch: Optional[pyglet.graphics.Batch] = None,
) -> None: ) -> None:
self.screen = screen
self.x = x self.x = x
self.y = y self.y = y
self.width = width self.width = width
@ -30,80 +34,98 @@ class Plot:
self.bars = bars self.bars = bars
self.bar_width = bar_width self.bar_width = bar_width
self.color = color self.color = color
self.batch = batch or pyglet.graphics.Batch()
self.lines: List[pyglet.shapes.Line] = []
self.audio = b"" self.audio = b""
def update(self): def draw(self) -> None:
data = np.fromstring(self.audio, dtype=np.int16) data = np.fromstring(self.audio, dtype=np.int16)
if len(data) == 0:
return
fft = abs(np.fft.fft(data).real) fft = abs(np.fft.fft(data).real)
fft = fft[: len(fft) // 2] fft = fft[: len(fft) // 2]
heights = scipy.signal.resample(fft, self.bars) * self.height / 2 ** 16 heights = scipy.signal.resample(fft, self.bars) * self.height / 2 ** 16
self.lines = [ for i, height in enumerate(heights):
pyglet.shapes.Line( pygame.draw.rect(
self.x + x / self.bars * self.width, self.screen,
self.color,
(
self.x + i / self.bars * self.width,
self.height,
self.bar_width,
-height,
),
0, 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(): def main():
window = pyglet.window.Window(fullscreen=True) app = application.Application()
with application.run() as events: config = app.config.get("gui", dict())
audio = b"" FPS = int(config.get("fps", 30))
label = pyglet.text.Label( WIDTH = int(config.get("width", 800))
"<Idle>", HEIGHT = int(config.get("height", 600))
font_name="Noto Sans", disp_no = os.getenv("DISPLAY")
font_size=36, if disp_no:
x=window.width // 2, logger.info("I'm running under X display = {0}".format(disp_no))
y=window.height // 2,
anchor_x="center", # Check which frame buffer drivers are available
anchor_y="center", # Start with fbcon since directfb hangs with composite output
) drivers = ["x11", "fbcon", "directfb", "svgalib"]
batch = pyglet.graphics.Batch() 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
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( plot = Plot(
screen=screen,
x=0, x=0,
y=0, y=0,
width=window.width, width=screen.get_width(),
height=window.height, height=screen.get_height(),
bars=40, bars=40,
bar_width=window.width // 45, bar_width=screen.get_width() // 45,
color=(139, 0, 139), color=(139, 0, 139),
batch=batch,
) )
@window.event app.run()
def on_draw(): clock = pygame.time.Clock()
window.clear() while True:
batch.draw() for event in pygame.event.get():
label.draw() if event.type == QUIT or (event.type == KEYDOWN and event.key == K_ESCAPE):
app.shutdown()
def check_events(dt): pygame.quit()
return
try: try:
event = events.get(False) while event := app.events.get(False):
if isinstance(event, turntable.StartedPlaying): ...
label.text = "<Record starting...>"
elif isinstance(event, turntable.StoppedPlaying):
label.text = "<Idle>"
elif isinstance(event, turntable.NewMetadata):
label.text = event.title
elif isinstance(event, turntable.Audio):
plot.audio = event.pcm.raw
except queue.Empty: except queue.Empty:
... ...
try:
def update_vis(dt): while pcm := app.pcm_display.get(False):
plot.update() plot.audio = pcm.raw
except queue.Empty:
pyglet.clock.schedule(check_events) ...
pyglet.clock.schedule_interval(update_vis, 0.03) screen.fill((0, 0, 0))
pyglet.app.run() plot.draw()
pygame.display.update()
clock.tick(30)