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,63 +23,69 @@ 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]]":
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) class Application:
logger.info("Turntable version %s", VERSION) 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_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),
period_size=audio_config.get("period_size", 4096), period_size=audio_config.get("period_size", 4096),
) )
player = Player( player = Player(
pcm_out, pcm_out,
audio_config.get("output_device", "null"), audio_config.get("output_device", "null"),
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),
) )
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),
mountpoint=icecast_config.get("mountpoint", "stream.mp3"), mountpoint=icecast_config.get("mountpoint", "stream.mp3"),
user=icecast_config.get("admin_user", "admin"), user=icecast_config.get("admin_user", "admin"),
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:
process.daemon = True def run(self) -> "Iterator[Queue[Event]]":
process.start() for process in self.processes:
try: process.daemon = True
yield events process.start()
except:
logging.exception("Terminating") def shutdown(self) -> None:
for process in processes: logging.info("Terminating")
for process in self.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",
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,
)
@window.event # Check which frame buffer drivers are available
def on_draw(): # Start with fbcon since directfb hangs with composite output
window.clear() drivers = ["x11", "fbcon", "directfb", "svgalib"]
batch.draw() found = False
label.draw() 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): if not found:
try: raise Exception("No suitable video driver found!")
event = events.get(False)
if isinstance(event, turntable.StartedPlaying): size = (pygame.display.Info().current_w, pygame.display.Info().current_h)
label.text = "<Record starting...>" logger.info("Window size: %d x %d" % (size[0], size[1]))
elif isinstance(event, turntable.StoppedPlaying): screen = pygame.display.set_mode((WIDTH, HEIGHT))
label.text = "<Idle>" # Clear the screen to start
elif isinstance(event, turntable.NewMetadata): screen.fill((0, 0, 0))
label.text = event.title # Initialise font support
elif isinstance(event, turntable.Audio): pygame.font.init()
plot.audio = event.pcm.raw # Render the screen
except queue.Empty: 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):
... ...
except queue.Empty:
def update_vis(dt): ...
plot.update() try:
while pcm := app.pcm_display.get(False):
pyglet.clock.schedule(check_events) plot.audio = pcm.raw
pyglet.clock.schedule_interval(update_vis, 0.03) except queue.Empty:
pyglet.app.run() ...
screen.fill((0, 0, 0))
plot.draw()
pygame.display.update()
clock.tick(30)