improve selection of audio source

This commit is contained in:
pstruebi
2025-07-28 18:04:49 +02:00
parent d7bb9132ea
commit cff06439d1
7 changed files with 115 additions and 80 deletions

View File

@@ -138,5 +138,27 @@ sudo systemctl status auracast-frontend
If you want to run the services as a specific user, edit the `User=` line in the service files accordingly. If you want to run the services as a specific user, edit the `User=` line in the service files accordingly.
# install port audio so it can see pipewire devices on raspian
sudo apt update
sudo apt install --no-install-recommends \
git build-essential cmake pkg-config \
libasound2-dev libpulse-dev libjack-jackd2-dev
git clone https://github.com/PortAudio/portaudio.git
cd portaudio # branch = master (19.7devel)
rm -rf build
cmake -S . -B build -G"Unix Makefiles" \
-DBUILD_SHARED_LIBS=ON \
-DPA_USE_ALSA=ON \
-DPA_USE_OSS=ON \
-DPA_USE_PULSEAUDIO=ON \
-DPA_USE_JACK=OFF
cmake --build build -j$(nproc)
sudo apt remove -y libportaudio2 portaudio19-dev libportaudiocpp0
sudo cmake --install build # installs to /usr/local/lib
sudo ldconfig # refresh linker cache
# Known issues: # Known issues:
- When running on a laptop there might be issues switching between usb and browser audio input since they use the same audio device - When running on a laptop there might be issues switching between usb and browser audio input since they use the same audio device

30
poetry.lock generated
View File

@@ -1476,27 +1476,43 @@ files = [
{file = "pandas-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6872d695c896f00df46b71648eea332279ef4077a409e2fe94220208b6bb675"}, {file = "pandas-2.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a6872d695c896f00df46b71648eea332279ef4077a409e2fe94220208b6bb675"},
{file = "pandas-2.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4dd97c19bd06bc557ad787a15b6489d2614ddaab5d104a0310eb314c724b2d2"}, {file = "pandas-2.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4dd97c19bd06bc557ad787a15b6489d2614ddaab5d104a0310eb314c724b2d2"},
{file = "pandas-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:034abd6f3db8b9880aaee98f4f5d4dbec7c4829938463ec046517220b2f8574e"}, {file = "pandas-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:034abd6f3db8b9880aaee98f4f5d4dbec7c4829938463ec046517220b2f8574e"},
{file = "pandas-2.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23c2b2dc5213810208ca0b80b8666670eb4660bbfd9d45f58592cc4ddcfd62e1"},
{file = "pandas-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:39ff73ec07be5e90330cc6ff5705c651ace83374189dcdcb46e6ff54b4a72cd6"}, {file = "pandas-2.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:39ff73ec07be5e90330cc6ff5705c651ace83374189dcdcb46e6ff54b4a72cd6"},
{file = "pandas-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:40cecc4ea5abd2921682b57532baea5588cc5f80f0231c624056b146887274d2"}, {file = "pandas-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:40cecc4ea5abd2921682b57532baea5588cc5f80f0231c624056b146887274d2"},
{file = "pandas-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8adff9f138fc614347ff33812046787f7d43b3cef7c0f0171b3340cae333f6ca"}, {file = "pandas-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8adff9f138fc614347ff33812046787f7d43b3cef7c0f0171b3340cae333f6ca"},
{file = "pandas-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e5f08eb9a445d07720776df6e641975665c9ea12c9d8a331e0f6890f2dcd76ef"},
{file = "pandas-2.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa35c266c8cd1a67d75971a1912b185b492d257092bdd2709bbdebe574ed228d"}, {file = "pandas-2.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa35c266c8cd1a67d75971a1912b185b492d257092bdd2709bbdebe574ed228d"},
{file = "pandas-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a0cc77b0f089d2d2ffe3007db58f170dae9b9f54e569b299db871a3ab5bf46"}, {file = "pandas-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a0cc77b0f089d2d2ffe3007db58f170dae9b9f54e569b299db871a3ab5bf46"},
{file = "pandas-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c06f6f144ad0a1bf84699aeea7eff6068ca5c63ceb404798198af7eb86082e33"},
{file = "pandas-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ed16339bc354a73e0a609df36d256672c7d296f3f767ac07257801aa064ff73c"}, {file = "pandas-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ed16339bc354a73e0a609df36d256672c7d296f3f767ac07257801aa064ff73c"},
{file = "pandas-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:fa07e138b3f6c04addfeaf56cc7fdb96c3b68a3fe5e5401251f231fce40a0d7a"}, {file = "pandas-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:fa07e138b3f6c04addfeaf56cc7fdb96c3b68a3fe5e5401251f231fce40a0d7a"},
{file = "pandas-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2eb4728a18dcd2908c7fccf74a982e241b467d178724545a48d0caf534b38ebf"}, {file = "pandas-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2eb4728a18dcd2908c7fccf74a982e241b467d178724545a48d0caf534b38ebf"},
{file = "pandas-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9d8c3187be7479ea5c3d30c32a5d73d62a621166675063b2edd21bc47614027"},
{file = "pandas-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ff730713d4c4f2f1c860e36c005c7cefc1c7c80c21c0688fd605aa43c9fcf09"},
{file = "pandas-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba24af48643b12ffe49b27065d3babd52702d95ab70f50e1b34f71ca703e2c0d"}, {file = "pandas-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba24af48643b12ffe49b27065d3babd52702d95ab70f50e1b34f71ca703e2c0d"},
{file = "pandas-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:404d681c698e3c8a40a61d0cd9412cc7364ab9a9cc6e144ae2992e11a2e77a20"},
{file = "pandas-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6021910b086b3ca756755e86ddc64e0ddafd5e58e076c72cb1585162e5ad259b"}, {file = "pandas-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6021910b086b3ca756755e86ddc64e0ddafd5e58e076c72cb1585162e5ad259b"},
{file = "pandas-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:094e271a15b579650ebf4c5155c05dcd2a14fd4fdd72cf4854b2f7ad31ea30be"}, {file = "pandas-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:094e271a15b579650ebf4c5155c05dcd2a14fd4fdd72cf4854b2f7ad31ea30be"},
{file = "pandas-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983"}, {file = "pandas-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7e2fc25f89a49a11599ec1e76821322439d90820108309bf42130d2f36c983"},
{file = "pandas-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6da97aeb6a6d233fb6b17986234cc723b396b50a3c6804776351994f2a658fd"},
{file = "pandas-2.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f"}, {file = "pandas-2.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb32dc743b52467d488e7a7c8039b821da2826a9ba4f85b89ea95274f863280f"},
{file = "pandas-2.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3"}, {file = "pandas-2.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:213cd63c43263dbb522c1f8a7c9d072e25900f6975596f883f4bebd77295d4f3"},
{file = "pandas-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1d2b33e68d0ce64e26a4acc2e72d747292084f4e8db4c847c6f5f6cbe56ed6d8"},
{file = "pandas-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9"}, {file = "pandas-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:430a63bae10b5086995db1b02694996336e5a8ac9a96b4200572b413dfdfccb9"},
{file = "pandas-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390"}, {file = "pandas-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4930255e28ff5545e2ca404637bcc56f031893142773b3468dc021c6c32a1390"},
{file = "pandas-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575"}, {file = "pandas-2.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f925f1ef673b4bd0271b1809b72b3270384f2b7d9d14a189b12b7fc02574d575"},
{file = "pandas-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042"}, {file = "pandas-2.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78ad363ddb873a631e92a3c063ade1ecfb34cae71e9a2be6ad100f875ac1042"},
{file = "pandas-2.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c"}, {file = "pandas-2.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951805d146922aed8357e4cc5671b8b0b9be1027f0619cea132a9f3f65f2f09c"},
{file = "pandas-2.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67"}, {file = "pandas-2.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a881bc1309f3fce34696d07b00f13335c41f5f5a8770a33b09ebe23261cfc67"},
{file = "pandas-2.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1991bbb96f4050b09b5f811253c4f3cf05ee89a589379aa36cd623f21a31d6f"},
{file = "pandas-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249"}, {file = "pandas-2.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bb3be958022198531eb7ec2008cfc78c5b1eed51af8600c6c5d9160d89d8d249"},
{file = "pandas-2.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9efc0acbbffb5236fbdf0409c04edce96bec4bdaa649d49985427bd1ec73e085"},
{file = "pandas-2.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75651c14fde635e680496148a8526b328e09fe0572d9ae9b638648c46a544ba3"},
{file = "pandas-2.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5be867a0541a9fb47a4be0c5790a4bccd5b77b92f0a59eeec9375fafc2aa14"},
{file = "pandas-2.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84141f722d45d0c2a89544dd29d35b3abfc13d2250ed7e68394eda7564bd6324"},
{file = "pandas-2.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f95a2aef32614ed86216d3c450ab12a4e82084e8102e355707a1d96e33d51c34"},
{file = "pandas-2.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e0f51973ba93a9f97185049326d75b942b9aeb472bec616a129806facb129ebb"},
{file = "pandas-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:b198687ca9c8529662213538a9bb1e60fa0bf0f6af89292eb68fea28743fcd5a"},
{file = "pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133"}, {file = "pandas-2.3.0.tar.gz", hash = "sha256:34600ab34ebf1131a7613a260a61dbe8b62c188ec0ea4c296da7c9a06b004133"},
] ]
@@ -2448,17 +2464,17 @@ files = [
[[package]] [[package]]
name = "sounddevice" name = "sounddevice"
version = "0.5.1" version = "0.5.2"
description = "Play and Record Sound with Python" description = "Play and Record Sound with Python"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "sounddevice-0.5.1-py3-none-any.whl", hash = "sha256:e2017f182888c3f3c280d9fbac92e5dbddac024a7e3442f6e6116bd79dab8a9c"}, {file = "sounddevice-0.5.2-py3-none-any.whl", hash = "sha256:82375859fac2e73295a4ab3fc60bd4782743157adc339561c1f1142af472f505"},
{file = "sounddevice-0.5.1-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:d16cb23d92322526a86a9490c427bf8d49e273d9ccc0bd096feecd229cde6031"}, {file = "sounddevice-0.5.2-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:943f27e66037d41435bdd0293454072cdf657b594c9cde63cd01ee3daaac7ab3"},
{file = "sounddevice-0.5.1-py3-none-win32.whl", hash = "sha256:d84cc6231526e7a08e89beff229c37f762baefe5e0cc2747cbe8e3a565470055"}, {file = "sounddevice-0.5.2-py3-none-win32.whl", hash = "sha256:3a113ce614a2c557f14737cb20123ae6298c91fc9301eb014ada0cba6d248c5f"},
{file = "sounddevice-0.5.1-py3-none-win_amd64.whl", hash = "sha256:4313b63f2076552b23ac3e0abd3bcfc0c1c6a696fc356759a13bd113c9df90f1"}, {file = "sounddevice-0.5.2-py3-none-win_amd64.whl", hash = "sha256:e18944b767d2dac3771a7771bdd7ff7d3acd7d334e72c4bedab17d1aed5dbc22"},
{file = "sounddevice-0.5.1.tar.gz", hash = "sha256:09ca991daeda8ce4be9ac91e15a9a81c8f81efa6b695a348c9171ea0c16cb041"}, {file = "sounddevice-0.5.2.tar.gz", hash = "sha256:c634d51bd4e922d6f0fa5e1a975cc897c947f61d31da9f79ba7ea34dff448b49"},
] ]
[package.dependencies] [package.dependencies]
@@ -2903,4 +2919,4 @@ test = ["pytest", "pytest-asyncio"]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.11" python-versions = ">=3.11"
content-hash = "ad107b21cc3f0f5e78c4bb2dccd12cf320d37b2d3f262618796d848b05d06086" content-hash = "ee5d8e347947d5b3651aa1b9bcfb8b952e706d0a4a7d28bc0a9d3e5526d82c16"

View File

@@ -6,16 +6,15 @@ requires-python = ">=3.11"
dependencies = [ dependencies = [
"bumble @ git+ssh://git@ssh.pstruebi.xyz:222/auracaster/bumble_mirror.git@12bcdb7770c0d57a094bc0a96cd52e701f97fece", "bumble @ git+ssh://git@ssh.pstruebi.xyz:222/auracaster/bumble_mirror.git@12bcdb7770c0d57a094bc0a96cd52e701f97fece",
"lc3 @ git+ssh://git@ssh.pstruebi.xyz:222/auracaster/liblc3.git@7558637303106c7ea971e7bb8cedf379d3e08bcc", "lc3 @ git+ssh://git@ssh.pstruebi.xyz:222/auracaster/liblc3.git@7558637303106c7ea971e7bb8cedf379d3e08bcc",
"sounddevice",
"aioconsole", "aioconsole",
"fastapi==0.115.11", "fastapi==0.115.11",
"uvicorn==0.34.0", "uvicorn==0.34.0",
"aiohttp==3.9.3", "aiohttp==3.9.3",
"sounddevice (>=0.5.1,<0.6.0)",
"aioconsole (>=0.8.1,<0.9.0)", "aioconsole (>=0.8.1,<0.9.0)",
"numpy (>=2.2.6,<3.0.0)", "numpy (>=2.2.6,<3.0.0)",
"streamlit (>=1.45.1,<2.0.0)", "streamlit (>=1.45.1,<2.0.0)",
"aiortc (>=1.13.0,<2.0.0)" "aiortc (>=1.13.0,<2.0.0)",
"sounddevice (>=0.5.2,<0.6.0)"
] ]
[project.optional-dependencies] [project.optional-dependencies]

View File

@@ -203,8 +203,21 @@ else:
# Input device selection for USB mode # Input device selection for USB mode
if audio_mode == "USB/Network": if audio_mode == "USB/Network":
resp = requests.get(f"{BACKEND_URL}/audio_inputs") resp = requests.get(f"{BACKEND_URL}/audio_inputs")
input_options = [f"{d['id']}:{d['name']}" for d in resp.json().get('inputs', [])] device_list = resp.json().get('inputs', [])
# Display "name [id]" but use name as value
input_options = [f"{d['name']} [{d['id']}]" for d in device_list]
option_name_map = {f"{d['name']} [{d['id']}]": d['name'] for d in device_list}
device_names = [d['name'] for d in device_list]
# Determine default input by name
default_input_name = saved_settings.get('input_device')
if default_input_name not in device_names and device_names:
default_input_name = device_names[0]
default_input_label = None
for label, name in option_name_map.items():
if name == default_input_name:
default_input_label = label
break
if not input_options: if not input_options:
st.warning("No hardware audio input devices found. Plug in a USB input device and click Refresh.") st.warning("No hardware audio input devices found. Plug in a USB input device and click Refresh.")
if st.button("Refresh"): if st.button("Refresh"):
@@ -215,11 +228,13 @@ else:
st.rerun() st.rerun()
input_device = None input_device = None
else: else:
if default_input not in input_options:
default_input = input_options[0]
col1, col2 = st.columns([3, 1], vertical_alignment="bottom") col1, col2 = st.columns([3, 1], vertical_alignment="bottom")
with col1: with col1:
selected_option = st.selectbox("Input Device", input_options, index=input_options.index(default_input)) selected_option = st.selectbox(
"Input Device",
input_options,
index=input_options.index(default_input_label) if default_input_label in input_options else 0
)
with col2: with col2:
if st.button("Refresh"): if st.button("Refresh"):
try: try:
@@ -227,8 +242,8 @@ else:
except Exception as e: except Exception as e:
st.error(f"Failed to refresh devices: {e}") st.error(f"Failed to refresh devices: {e}")
st.rerun() st.rerun()
# We send only the numeric/card identifier (before :) or 'default' # Send only the device name to backend
input_device = selected_option.split(":", 1)[0] if ":" in selected_option else selected_option input_device = option_name_map[selected_option] if selected_option in option_name_map else None
else: else:
input_device = None input_device = None

View File

@@ -27,6 +27,13 @@ class Offer(BaseModel):
sdp: str sdp: str
type: str type: str
def get_device_index_by_name(name: str):
"""Return the device index for a given device name, or None if not found."""
for d in AUDIO_INPUT_DEVICES_CACHE:
if d["name"] == name:
return d["id"]
return None
# Path to persist stream settings # Path to persist stream settings
STREAM_SETTINGS_FILE = os.path.join(os.path.dirname(__file__), 'stream_settings.json') STREAM_SETTINGS_FILE = os.path.join(os.path.dirname(__file__), 'stream_settings.json')
@@ -50,6 +57,9 @@ def save_stream_settings(settings: dict):
log.error('Unable to persist stream settings: %s', e) log.error('Unable to persist stream settings: %s', e)
# TODO: select this dynamically from pw-cli ls Node determine id
os.environ["PIPEWIRE_NODE"] = "88"
app = FastAPI() app = FastAPI()
# Allow CORS for frontend on localhost # Allow CORS for frontend on localhost
@@ -83,26 +93,36 @@ async def initialize(conf: auracast_config.AuracastConfigGroup):
conf.transport = f'serial:{device},115200,rtscts' conf.transport = f'serial:{device},115200,rtscts'
break break
if conf.transport == 'auto': if conf.transport == 'auto':
HTTPException(status_code=500, detail='No suitable transport found.') raise HTTPException(status_code=500, detail='No suitable transport found.')
# Derive audio_mode and input_device from first BIG audio_source # Derive audio_mode and input_device from first BIG audio_source
first_source = conf.bigs[0].audio_source if conf.bigs else '' first_source = conf.bigs[0].audio_source if conf.bigs else ''
if first_source.startswith('device:'): if first_source.startswith('device:'):
audio_mode_persist = 'USB' audio_mode_persist = 'USB'
input_device = first_source.split(':', 1)[1] if ':' in first_source else 'default' input_device_name = first_source.split(':', 1)[1] if ':' in first_source else None
# Map device name to current index for use with sounddevice
device_index = get_device_index_by_name(input_device_name) if input_device_name else None
# Patch config to use index for sounddevice (but persist name)
if device_index is not None:
for big in conf.bigs:
if big.audio_source.startswith('device:'):
big.audio_source = f'device:{device_index}'
else:
log.error(f"Device name '{input_device_name}' not found in current device list.")
raise HTTPException(status_code=400, detail=f"Audio device '{input_device_name}' not found.")
elif first_source == 'webrtc': elif first_source == 'webrtc':
audio_mode_persist = 'Webapp' audio_mode_persist = 'Webapp'
input_device = None input_device_name = None
elif first_source.startswith('file:'): elif first_source.startswith('file:'):
audio_mode_persist = 'Demo' audio_mode_persist = 'Demo'
input_device = None input_device_name = None
else: else:
audio_mode_persist = 'Network' audio_mode_persist = 'Network'
input_device = None input_device_name = None
save_stream_settings({ save_stream_settings({
'channel_names': [big.name for big in conf.bigs], 'channel_names': [big.name for big in conf.bigs],
'languages': [big.language for big in conf.bigs], 'languages': [big.language for big in conf.bigs],
'audio_mode': audio_mode_persist, 'audio_mode': audio_mode_persist,
'input_device': input_device, 'input_device': input_device_name,
'program_info': [getattr(big, 'program_info', None) for big in conf.bigs], 'program_info': [getattr(big, 'program_info', None) for big in conf.bigs],
'gain': [getattr(big, 'input_gain', 1.0) for big in conf.bigs], 'gain': [getattr(big, 'input_gain', 1.0) for big in conf.bigs],
'timestamp': datetime.utcnow().isoformat() 'timestamp': datetime.utcnow().isoformat()
@@ -137,12 +157,17 @@ async def initialize2(conf: auracast_config.AuracastConfigGroup):
conf.transport = f'serial:{device},115200,rtscts' conf.transport = f'serial:{device},115200,rtscts'
break break
if conf.transport == 'auto': if conf.transport == 'auto':
HTTPException(status_code=500, detail='No suitable transport found.') raise HTTPException(status_code=500, detail='No suitable transport found.')
if multicaster2 is not None: # Patch device name to index for sounddevice
try: for big in conf.bigs:
await multicaster2.shutdown() if big.audio_source.startswith('device:'):
except Exception: device_name = big.audio_source.split(':', 1)[1]
log.warning("Failed to shutdown previous multicaster2", exc_info=True) device_index = get_device_index_by_name(device_name)
if device_index is not None:
big.audio_source = f'device:{device_index}'
else:
log.error(f"Device name '{device_name}' not found in current device list.")
raise HTTPException(status_code=400, detail=f"Audio device '{device_name}' not found.")
log.info('Initializing multicaster2 with config:\n %s', conf.model_dump_json(indent=2)) log.info('Initializing multicaster2 with config:\n %s', conf.model_dump_json(indent=2))
multicaster2 = multicast_control.Multicaster(conf, conf.bigs) multicaster2 = multicast_control.Multicaster(conf, conf.bigs)
await multicaster2.init_broadcast() await multicaster2.init_broadcast()
@@ -220,13 +245,11 @@ async def scan_audio_devices():
sd._terminate() sd._terminate()
sd._initialize() sd._initialize()
# TODO: select this dynamically from pw-cli ls Node determine id
#os.environ["PIPEWIRE_NODE"] = "76"
devs = sd.query_devices() devs = sd.query_devices()
inputs = [ inputs = [
{"id": idx, "name": d["name"]} {"id": idx, "name": d["name"]}
for idx, d in enumerate(devs) for idx, d in enumerate(devs)
# if d.get("max_input_channels", 0) > 0 and ("(hw:" in d["name"].lower() or "usb" in d["name"].lower()) if d.get("max_input_channels", 0) > 0
] ]
log.info('Found %d audio input devices: %s', len(inputs), inputs) log.info('Found %d audio input devices: %s', len(inputs), inputs)
AUDIO_INPUT_DEVICES_CACHE = inputs AUDIO_INPUT_DEVICES_CACHE = inputs
@@ -243,8 +266,9 @@ async def startup_event():
@app.get("/audio_inputs") @app.get("/audio_inputs")
async def list_audio_inputs(): async def list_audio_inputs():
"""Return available hardware audio input devices from cache.""" """Return available hardware audio input devices from cache (by name, for selection)."""
return {"inputs": AUDIO_INPUT_DEVICES_CACHE} # Only expose name and id for frontend
return {"inputs": [{"name": d["name"], "id": d["id"]} for d in AUDIO_INPUT_DEVICES_CACHE]}
@app.post("/refresh_audio_inputs") @app.post("/refresh_audio_inputs")

View File

@@ -0,0 +1,4 @@
import sounddevice as sd, pprint
print(sd._libname) # → /usr/local/lib/libportaudio.so.2
print(sd.get_portaudio_version()) # 19.7.0devel (or newer)
pprint.pprint(sd.query_devices()) # every PipeWire sink/source appears

View File

@@ -87,7 +87,7 @@ context.modules = [
media.class = "Audio/Source" media.class = "Audio/Source"
device.api = aes67 device.api = aes67
# You can adjust the latency buffering here. Use integer values only # You can adjust the latency buffering here. Use integer values only
sess.latency.msec = 50 sess.latency.msec = 5
node.group = pipewire.ptp0 node.group = pipewire.ptp0
} }
} }
@@ -105,49 +105,4 @@ context.modules = [
] ]
} }
}, },
{ name = libpipewire-module-rtp-sink
args = {
### Please select the interface here
local.ifname = eth0
### If you want to create multiple output streams, please copy the whole
### module-rtp-sink block, but change this multicast IP to another unused
### one keeping 239.69.x.x range unless you know you need another one
destination.ip = 239.69.150.243
destination.port = 5004
net.mtu = 1280
net.ttl = 32
net.loop = false
# These should typically be equal
# You can customize packet length, but 1 ms should work for every device
# Consult receiver documentation to ensure it supports the value you set
sess.min-ptime = 1
sess.max-ptime = 1
### Please change this, especially if you create multiple sinks
sess.name = "PipeWire RTP stream"
sess.media = "audio"
# This property is used if you aren't using ptp4l 4
sess.ts-refclk = "ptp=traceable"
sess.ts-offset = 0
# You can adjust the latency buffering here. Use integer values only
sess.latency.msec = 3
audio.format = "S24BE"
audio.rate = 48000
audio.channels = 2
# These channel names will be visible both to applications and AES67 receivers
node.channel-names = ["CH1", "CH2"]
stream.props = {
### Please change the sink name, this is necessary when you create multiple sinks
node.name = "rtp-sink"
media.class = "Audio/Sink"
node.virtual = false
device.api = aes67
sess.sap.announce = true
node.always-process = true
node.group = pipewire.ptp0
rtp.ntp = 0
rtp.fetch-ts-refclk = true
}
}
},
] ]