Refactor audio input device selection logic (#897)

* Refactor audio input device selection logic

* add audio input device check in __main__.py and audio.py.
This commit is contained in:
Shaun Eccles-Smith 2024-04-07 10:18:28 +10:00 committed by GitHub
parent 98ffbd83cb
commit 49dc34bd5a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 59 additions and 9 deletions

View File

@ -110,7 +110,6 @@ jobs:
- name: Install build dependencies
run: |
brew install portaudio
- name: Setup Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5

View File

@ -14,6 +14,7 @@ import os
import sys
from logging.handlers import RotatingFileHandler
from ledfx.effects.audio import AudioAnalysisSource
from ledfx.sentry_config import setup_sentry
try:
@ -232,7 +233,6 @@ def main():
# Set some process priority optimisations
if have_psutil:
p = psutil.Process(os.getpid())
if psutil.WINDOWS:
try:
p.nice(psutil.HIGH_PRIORITY_CLASS)
@ -247,7 +247,7 @@ def main():
p.ionice(psutil.IOPRIO_CLASS_RT, value=7)
except psutil.Error:
_LOGGER.info(
"Unable to set priority, please run as root or sudo if you are experiencing frame rate issues",
"Unable to set priority, please run as root or use sudo if you are experiencing frame rate issues",
)
else:
p.nice(15)
@ -256,18 +256,30 @@ def main():
setup_sentry()
if args.sentry_test:
"""This will crash LedFx and submit a Sentry error if Sentry is configured"""
_LOGGER.warning("Steering LedFx into a brick wall")
_LOGGER.warning("Steering LedFx into a brick wall.")
div_by_zero = 1 / 0
# Check if there are any audio input devices and quit if there are none.
# TODO: Review the sentry hits for this logger statement and see if it's worth supporting without a mic.
# NOTE: We don't do this in CI - some runners don't have audio devices.
if (
AudioAnalysisSource.audio_input_device_exists() is False
and args.ci_smoke_test is False
):
_LOGGER.critical(
"No audio input devices found. Please connect a microphone or input device and restart LedFx."
)
# Exit with code 2 to indicate that there are no audio input devices.
sys.exit(2)
if (args.tray or currently_frozen()) and not args.no_tray:
# If pystray is imported on a device that can't display it, it explodes. Catch it
try:
import pystray
except Exception as Error:
msg = f"Error: Unable to virtual tray icon. Shutting down. Error: {Error}"
msg = f"Unable to create tray icon. Error: {Error}. Try launching LedFx via --no-tray option."
_LOGGER.critical(msg)
sys.exit(0)
# Exit with code 3 to indicate that there was an error creating the tray icon.
sys.exit(3)
from PIL import Image

View File

@ -360,6 +360,7 @@ def ensure_config_directory(config_dir: str) -> None:
_LOGGER.critical(
f"Unable to create configuration directory at {config_dir}. Shutting down."
)
# Exit with code 1 to indicate that there was an error creating the configuration directory.
sys.exit(1)

View File

@ -53,6 +53,13 @@ class AudioInputSource:
"""
return tuple(AudioInputSource.input_devices().keys())
@staticmethod
def audio_input_device_exists():
"""
Returns True if there are valid input devices
"""
return len(AudioInputSource.valid_device_indexes()) > 0
@staticmethod
def default_device_index():
"""
@ -68,12 +75,34 @@ class AudioInputSource:
f"{device_list[default_output_device]['name']} [Loopback]"
)
# We need to run over the device list looking for the target device
# NOTE: Some sound drivers truncate the device name, so we may not find a match
for device_index, device in enumerate(device_list):
if device["name"] == target_device:
# Return the loopback device index
_LOGGER.debug(
f"Default audio loopback device found: {device['name']}"
)
return device_index
# No Loopback device matching output found - return the default input device index
return sd.default.device["input"]
# If we don't match a Loopback device matching output found - return the default input device index
default_input_device_idx = sd.default.device["input"]
# The default input device index is not always valid (i.e no default input devices)
if default_input_device_idx in AudioInputSource.valid_device_indexes():
_LOGGER.debug(
"No default audio loopback device found. Using default input device."
)
return default_input_device_idx
else:
# Return the first valid input device index if we can't find a valid default input device
if len(AudioInputSource.valid_device_indexes()) > 0:
_LOGGER.debug(
"No valid default audio input device found. Using first valid input device."
)
return next(iter(AudioInputSource.valid_device_indexes()))
else:
_LOGGER.warning(
"No valid audio input devices found. Unable to use audio reactive effects."
)
return None
@staticmethod
def query_hostapis():
@ -181,6 +210,15 @@ class AudioInputSource:
input_devices = self.query_devices()
hostapis = self.query_hostapis()
default_device = self.default_device_index()
if default_device is None:
# There are no valid audio input devices, so we can't activate the audio source.
# We should never get here, as we check for devices on start-up.
# This likely just captures if a device is removed after start-up.
_LOGGER.warning(
"Audio input device not found. Unable to activate audio source. Deactivating."
)
self.deactivate()
return
valid_device_indexes = self.valid_device_indexes()
device_idx = self._config["audio_device"]