From 1ee542ea3ec06739a027dc27f7ef8f88373fd906 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 12 Oct 2021 20:45:29 +0100 Subject: [PATCH 1/7] Fix linux to use pulseaudio. --- .gitignore | 2 ++ dev/scripts/get_linux_outputs.py | 24 ++++++++++++++++++++++++ helpers/device_manager.py | 14 ++++++++------ package-lock.json | 12 ++++++++++-- package.json | 5 ++++- player.py | 5 ++++- server.py | 6 ++++-- web_server.py | 15 +++++++++------ 8 files changed, 65 insertions(+), 18 deletions(-) create mode 100644 dev/scripts/get_linux_outputs.py diff --git a/.gitignore b/.gitignore index 66518ac..772a7f1 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ music-tmp/ presenter-build + +node_modules/ \ No newline at end of file diff --git a/dev/scripts/get_linux_outputs.py b/dev/scripts/get_linux_outputs.py new file mode 100644 index 0000000..323b6f2 --- /dev/null +++ b/dev/scripts/get_linux_outputs.py @@ -0,0 +1,24 @@ +import os + +os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" +os.putenv('SDL_AUDIODRIVER', 'pulseaudio') +import pygame._sdl2 as sdl2 +import pygame +from pygame import mixer +#pygame.init() +import time +mixer.init(44100, -16, 2, 1024) +is_capture = 0 # zero to request playback devices, non-zero to request recording devices +num = sdl2.get_num_audio_devices(is_capture) +names = [str(sdl2.get_audio_device_name(i, is_capture), encoding="utf-8") for i in range(num)] +mixer.quit() +for i in names: + print(i) + mixer.init(44100, -16, 2, 1024, devicename=i) + print(mixer.get_init()) + mixer.music.load("/home/mstratford/Downloads/managed_play.mp3") + mixer.music.play() + #my_song = mixer.Sound("/home/mstratford/Downloads/managed_play.mp3") + #my_song.play() + time.sleep(5) + pygame.quit() \ No newline at end of file diff --git a/helpers/device_manager.py b/helpers/device_manager.py index ae5c930..a7871f9 100644 --- a/helpers/device_manager.py +++ b/helpers/device_manager.py @@ -20,10 +20,10 @@ class DeviceManager: return host_api @classmethod - def _getAudioDevices(cls) -> sd.DeviceList: + def _getSDAudioDevices(cls): # To update the list of devices - # Sadly this doesn't work on MacOS. - if not isMacOS(): + # Sadly this only works on Windows. Linux hangs, MacOS crashes. + if isWindows(): sd._terminate() sd._initialize() devices: sd.DeviceList = sd.query_devices() @@ -31,11 +31,13 @@ class DeviceManager: @classmethod def getAudioOutputs(cls) -> Tuple[List[Dict]]: - host_apis = sd.query_hostapis() - devices: sd.DeviceList = cls._getAudioDevices() + + host_apis = list(sd.query_hostapis()) + devices: sd.DeviceList = cls._getSDAudioDevices() for host_api_id in range(len(host_apis)): - if isWindows() and host_apis[host_api_id]["name"] not in WINDOWS_APIS: + # Linux SDL uses PortAudio, which SoundDevice doesn't find. So mark all as unsable. + if (isWindows() and host_apis[host_api_id]["name"] not in WINDOWS_APIS) or (isLinux()): host_apis[host_api_id]["usable"] = False else: host_apis[host_api_id]["usable"] = True diff --git a/package-lock.json b/package-lock.json index 939f4ff..ac7f15d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,13 @@ { "name": "bapsicle", - "version": "3.0.0", - "lockfileVersion": 1 + "version": "3.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "yarn": { + "version": "1.22.15", + "resolved": "https://registry.npmjs.org/yarn/-/yarn-1.22.15.tgz", + "integrity": "sha512-AzoEDxj256BOS/jqDXA3pjyhmi4FRBBUMgYoTHI4EIt2EhREkvH0soPVEtnD+DQIJfU5R9bKhcZ1H9l8zPWeoA==" + } + } } diff --git a/package.json b/package.json index cae6ea8..2d3161e 100644 --- a/package.json +++ b/package.json @@ -24,5 +24,8 @@ "bugs": { "url": "https://github.com/universityradioyork/bapsicle/issues" }, - "homepage": "https://github.com/universityradioyork/bapsicle#readme" + "homepage": "https://github.com/universityradioyork/bapsicle#readme", + "dependencies": { + "yarn": "^1.22.15" + } } diff --git a/player.py b/player.py index 5230432..0574168 100644 --- a/player.py +++ b/player.py @@ -21,8 +21,11 @@ # Stop the Pygame Hello message. import os - os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" +from helpers.os_environment import isLinux +# It's the only one we could get to work. +if isLinux(): + os.putenv('SDL_AUDIODRIVER', 'pulseaudio') from queue import Empty import multiprocessing diff --git a/server.py b/server.py index 82dde6e..4734dd4 100644 --- a/server.py +++ b/server.py @@ -23,7 +23,7 @@ import json from setproctitle import setproctitle import psutil -from helpers.os_environment import isMacOS +from helpers.os_environment import isLinux, isMacOS if not isMacOS(): # Rip, this doesn't like threading on MacOS. @@ -206,7 +206,9 @@ class BAPSicleServer: time.sleep(1) def startServer(self): - if isMacOS(): + # On MacOS, the default causes something to keep creating new processes. + # On Linux, this is needed to make pulseaudio initiate properly. + if isMacOS() or isLinux(): multiprocessing.set_start_method("spawn", True) process_title = "startServer" diff --git a/web_server.py b/web_server.py index 142db8a..1ae5068 100644 --- a/web_server.py +++ b/web_server.py @@ -540,9 +540,12 @@ def WebServer(player_to: List[Queue], player_from: List[Queue], state: StateMana ) except Exception: break - loop = asyncio.get_event_loop() - if loop: - loop.close() - if app: - app.stop() - del app + try: + loop = asyncio.get_event_loop() + if loop: + loop.close() + if app: + app.stop() + del app + except: + pass From e4cc6f7b61d8df6bcc79ecf41a25419323baa3b2 Mon Sep 17 00:00:00 2001 From: Marks Polakovs Date: Wed, 20 Oct 2021 20:49:19 +0100 Subject: [PATCH 2/7] Prompt before restarting the server if anything is playing --- ui-templates/restart-confirm.html | 9 +++++++++ web_server.py | 5 +++++ 2 files changed, 14 insertions(+) create mode 100644 ui-templates/restart-confirm.html diff --git a/ui-templates/restart-confirm.html b/ui-templates/restart-confirm.html new file mode 100644 index 0000000..3f7b3c6 --- /dev/null +++ b/ui-templates/restart-confirm.html @@ -0,0 +1,9 @@ +{% extends 'base.html' %} +{% block content_inner %} +
+

Hang on a second!

+

Something is currently playing. Restarting will interrupt it! Are you sure?

+ Cancel + Confirm +
+{% endblock %} diff --git a/web_server.py b/web_server.py index 142db8a..04ed686 100644 --- a/web_server.py +++ b/web_server.py @@ -496,6 +496,11 @@ def quit(request): @app.route("/restart") def restart(request): + if request.args.get("confirm", '') != "true": + for i in range(server_state.get()["num_channels"]): + state = status(i) + if state["playing"]: + return render_template("restart-confirm.html", data=None) server_state.update("running_state", "restarting") data = { From ecbfaa4a62955bef69b5818f9c352a36960bfa5e Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 2 Nov 2021 22:46:41 +0000 Subject: [PATCH 3/7] use sudo to install audio pkg --- build/build-linux.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build/build-linux.sh b/build/build-linux.sh index c67ee8d..9b27b8a 100755 --- a/build/build-linux.sh +++ b/build/build-linux.sh @@ -7,7 +7,7 @@ build_branch="$(git branch --show-current)" echo "BUILD: str = \"$build_commit\"" > ../build.py echo "BRANCH: str = \"$build_branch\"" >> ../build.py -apt install libportaudio2 +sudo apt install libportaudio2 python3 -m venv ../venv source ../venv/bin/activate @@ -19,6 +19,8 @@ pip3 install -e ../ python3 ./generate-build-exe-config.py +chmod +x output/BAPSicle + python3 ./build-exe.py bash ./build-exe-pyinstaller-command.sh From 592cf11a79141bedf015491214bbd6655c430263 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 2 Nov 2021 22:47:24 +0000 Subject: [PATCH 4/7] Fix mp3 support on linux with pygame 2.0.1 --- build/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/requirements.txt b/build/requirements.txt index 84c18be..015bd70 100644 --- a/build/requirements.txt +++ b/build/requirements.txt @@ -1,5 +1,5 @@ wheel -pygame==2.0.1 +pygame==2.0.2 sanic==21.3.4 sanic-Cors==1.0.0 syncer==1.3.0 From c475dbb5d5d72e87db75885a76aea4ff8e24de65 Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 2 Nov 2021 22:48:52 +0000 Subject: [PATCH 5/7] Switch to SDL outputs with pulseaudio for linux --- dev/scripts/get_linux_outputs.py | 2 +- helpers/device_manager.py | 15 +++++++++++++++ ui-templates/config_player.html | 21 +++++++++++++++++++++ web_server.py | 8 +++++++- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/dev/scripts/get_linux_outputs.py b/dev/scripts/get_linux_outputs.py index 323b6f2..6449302 100644 --- a/dev/scripts/get_linux_outputs.py +++ b/dev/scripts/get_linux_outputs.py @@ -5,7 +5,7 @@ os.putenv('SDL_AUDIODRIVER', 'pulseaudio') import pygame._sdl2 as sdl2 import pygame from pygame import mixer -#pygame.init() +pygame.init() import time mixer.init(44100, -16, 2, 1024) is_capture = 0 # zero to request playback devices, non-zero to request recording devices diff --git a/helpers/device_manager.py b/helpers/device_manager.py index a7871f9..aa5a7b0 100644 --- a/helpers/device_manager.py +++ b/helpers/device_manager.py @@ -1,6 +1,12 @@ from typing import Any, Dict, List, Optional, Tuple import sounddevice as sd from helpers.os_environment import isLinux, isMacOS, isWindows +import os + +os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" +os.putenv('SDL_AUDIODRIVER', 'pulseaudio') +import pygame._sdl2 as sdl2 +from pygame import mixer import glob if isWindows(): @@ -53,6 +59,15 @@ class DeviceManager: return host_apis + @classmethod + def getAudioDevices(cls) -> List[str]: + mixer.init(44100, -16, 2, 1024) + is_capture = 0 # zero to request playback devices, non-zero to request recording devices + num = sdl2.get_num_audio_devices(is_capture) + names = [str(sdl2.get_audio_device_name(i, is_capture), encoding="utf-8") for i in range(num)] + mixer.quit() + return names + @classmethod def getSerialPorts(cls) -> List[Optional[str]]: """Lists serial port names diff --git a/ui-templates/config_player.html b/ui-templates/config_player.html index 78d2c79..6ddbfd3 100644 --- a/ui-templates/config_player.html +++ b/ui-templates/config_player.html @@ -33,6 +33,26 @@ Set for: Default Audio Output

+{% if data.sdl_direct %} +Linux (Pulse Audio) +
+ +{% for output in data.outputs %} +Set for: + {% for channel in data.channels %} + {% if not channel %} + Player {{loop.index0}} + {% elif channel.output == output %} + Player {{channel.channel}} + {% else %} + Player {{channel.channel}} + {% endif %} + / + {% endfor %} +{% if output %}{{output}}{% else %}System Default Output{% endif %}
+{% endfor %} +
+{% else %} {% for host_api in data.outputs %} {{host_api.name}}
@@ -54,4 +74,5 @@ Default Audio Output {% endfor %} {% endfor %} +{% endif %} {% endblock %} diff --git a/web_server.py b/web_server.py index 1ae5068..5b809ef 100644 --- a/web_server.py +++ b/web_server.py @@ -17,6 +17,7 @@ import json import os from helpers.os_environment import ( + isLinux, resolve_external_file_path, resolve_local_file_path, ) @@ -171,11 +172,16 @@ def ui_config_player(request): for i in range(server_state.get()["num_channels"]): channel_states.append(status(i)) - outputs = DeviceManager.getAudioOutputs() + outputs = None + if isLinux(): + outputs = DeviceManager.getAudioDevices() + else: + outputs = DeviceManager.getAudioOutputs() data = { "channels": channel_states, "outputs": outputs, + "sdl_direct": isLinux(), "ui_page": "config", "ui_title": "Player Config", } From 65944e59b396bd0dd5a2cd50cd4494dfdaeb49da Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 2 Nov 2021 23:02:09 +0000 Subject: [PATCH 6/7] Fix lints. --- dev/scripts/get_linux_outputs.py | 6 +++--- web_server.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dev/scripts/get_linux_outputs.py b/dev/scripts/get_linux_outputs.py index 6449302..1c53024 100644 --- a/dev/scripts/get_linux_outputs.py +++ b/dev/scripts/get_linux_outputs.py @@ -18,7 +18,7 @@ for i in names: print(mixer.get_init()) mixer.music.load("/home/mstratford/Downloads/managed_play.mp3") mixer.music.play() - #my_song = mixer.Sound("/home/mstratford/Downloads/managed_play.mp3") - #my_song.play() + # my_song = mixer.Sound("/home/mstratford/Downloads/managed_play.mp3") + # my_song.play() time.sleep(5) - pygame.quit() \ No newline at end of file + pygame.quit() diff --git a/web_server.py b/web_server.py index 5b809ef..83acc14 100644 --- a/web_server.py +++ b/web_server.py @@ -181,7 +181,7 @@ def ui_config_player(request): data = { "channels": channel_states, "outputs": outputs, - "sdl_direct": isLinux(), + "sdl_direct": isLinux(), "ui_page": "config", "ui_title": "Player Config", } @@ -553,5 +553,5 @@ def WebServer(player_to: List[Queue], player_from: List[Queue], state: StateMana if app: app.stop() del app - except: + except Exception: pass From 0f25345a6a10bdd925263ed008c514f8c2b509bb Mon Sep 17 00:00:00 2001 From: Matthew Stratford Date: Tue, 2 Nov 2021 23:26:26 +0000 Subject: [PATCH 7/7] Fix pulseaudio error on !linux --- helpers/device_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/helpers/device_manager.py b/helpers/device_manager.py index aa5a7b0..df68409 100644 --- a/helpers/device_manager.py +++ b/helpers/device_manager.py @@ -4,7 +4,8 @@ from helpers.os_environment import isLinux, isMacOS, isWindows import os os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide" -os.putenv('SDL_AUDIODRIVER', 'pulseaudio') +if isLinux(): + os.putenv('SDL_AUDIODRIVER', 'pulseaudio') import pygame._sdl2 as sdl2 from pygame import mixer import glob