improve network audio selection
This commit is contained in:
@@ -1,5 +1,56 @@
|
||||
"""
|
||||
multicast_script
|
||||
=================
|
||||
|
||||
Loads environment variables from a .env file located next to this script
|
||||
and configures the multicast broadcast. Only UPPERCASE keys are read.
|
||||
|
||||
Environment variables
|
||||
---------------------
|
||||
- LOG_LEVEL: Logging level for the script.
|
||||
Default: INFO. Examples: DEBUG, INFO, WARNING, ERROR.
|
||||
|
||||
- INPUT: Select audio capture source.
|
||||
Values:
|
||||
- "usb" (default): first available USB input device.
|
||||
- "aes67": select AES67 inputs. Two forms:
|
||||
* INPUT=aes67 -> first available AES67 input.
|
||||
* INPUT=aes67,<substr> -> case-insensitive substring match against
|
||||
the device name, e.g. INPUT=aes67,8f6326.
|
||||
|
||||
- BROADCAST_NAME: Name of the broadcast (Auracast BIG name).
|
||||
Default: "Broadcast0".
|
||||
|
||||
- PROGRAM_INFO: Free-text program/broadcast info.
|
||||
Default: "Some Announcements".
|
||||
|
||||
- LANGUATE: ISO 639-3 language code used by config (intentional key name).
|
||||
Default: "deu".
|
||||
|
||||
- AURACAST_SD_BLOCKSIZE: Hint for PortAudio/PipeWire block size in frames.
|
||||
Default: 128.
|
||||
|
||||
- AURACAST_SD_LATENCY: PortAudio latency hint in seconds.
|
||||
Default: 0.0027 (~128/48000 s).
|
||||
|
||||
- PULSE_LATENCY_MSEC: Pulse/PipeWire latency hint in milliseconds.
|
||||
Default: 1.
|
||||
|
||||
- PIPEWIRE_LATENCY: PipeWire latency hint in the form "<frames>/<rate>".
|
||||
Default is initially set to "128/48000" and then overwritten at runtime to
|
||||
"128/<CAPTURE_SRATE>" based on the capture rate used by this script.
|
||||
|
||||
Examples (.env)
|
||||
---------------
|
||||
LOG_LEVEL=DEBUG
|
||||
INPUT=aes67,8f6326
|
||||
BROADCAST_NAME=MyBroadcast
|
||||
PROGRAM_INFO="Live announcements"
|
||||
LANGUATE=deu
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from dotenv import load_dotenv
|
||||
from auracast import multicast
|
||||
from auracast import auracast_config
|
||||
@@ -22,27 +73,49 @@ if __name__ == "__main__":
|
||||
os.environ.setdefault("AURACAST_SD_LATENCY", "0.0027") # ~128/48000 s
|
||||
os.environ.setdefault("PULSE_LATENCY_MSEC", "1")
|
||||
os.environ.setdefault("PIPEWIRE_LATENCY", "128/48000")
|
||||
logging.info("USB pw inputs:")
|
||||
|
||||
usb_inputs = list_usb_pw_inputs()
|
||||
logging.info("AEs67 pw inputs:")
|
||||
aes67_inputs = list_network_pw_inputs()
|
||||
logging.info("USB pw inputs:")
|
||||
for i, d in usb_inputs:
|
||||
logging.info(f"{i}: {d['name']} in={d['max_input_channels']}")
|
||||
|
||||
aes67_inputs = list_network_pw_inputs()
|
||||
logging.info("AES67 pw inputs:")
|
||||
for i, d in aes67_inputs:
|
||||
logging.info(f"{i}: {d['name']} in={d['max_input_channels']}")
|
||||
|
||||
# Input selection (usb | network). Default to usb.
|
||||
input_mode = (os.environ.get('INPUT', 'usb') or 'usb').lower()
|
||||
if input_mode == 'network':
|
||||
if aes67_inputs:
|
||||
input_sel = aes67_inputs[0][0]
|
||||
# Input selection (usb | aes67). Default to usb.
|
||||
# Allows specifying an AES67 device by substring: INPUT=aes67,<substring>
|
||||
# Example: INPUT=aes67,8f6326 will match a device name containing "8f6326".
|
||||
input_env = os.environ.get('INPUT', 'usb') or 'usb'
|
||||
parts = [p.strip() for p in input_env.split(',', 1)]
|
||||
input_mode = (parts[0] or 'usb').lower()
|
||||
iface_substr = (parts[1].lower() if len(parts) > 1 and parts[1] else None)
|
||||
|
||||
if input_mode == 'aes67':
|
||||
if not aes67_inputs and not iface_substr:
|
||||
# No AES67 inputs and no specific target -> fail fast
|
||||
raise RuntimeError("No AES67 audio inputs found.")
|
||||
if iface_substr:
|
||||
# Loop until a matching AES67 input becomes available
|
||||
while True:
|
||||
current = list_network_pw_inputs()
|
||||
sel = next(((i, d) for i, d in current if iface_substr in (d.get('name','').lower())), None)
|
||||
if sel:
|
||||
input_sel = sel[0]
|
||||
logging.info(f"Selected AES67 input by match '{iface_substr}': index={input_sel}")
|
||||
break
|
||||
logging.info(f"Waiting for AES67 input matching '{iface_substr}'... retrying in 2s")
|
||||
time.sleep(2)
|
||||
else:
|
||||
raise RuntimeError("No audio inputs found (USB or network).")
|
||||
input_sel = aes67_inputs[0][0]
|
||||
logging.info(f"Selected first AES67 input: index={input_sel}, device={aes67_inputs[0][1]['name']}")
|
||||
else:
|
||||
if usb_inputs:
|
||||
input_sel = usb_inputs[0][0]
|
||||
logging.info(f"Selected first USB input: index={input_sel}, device={usb_inputs[0][1]['name']}")
|
||||
else:
|
||||
raise RuntimeError("No audio inputs found (USB or network).")
|
||||
raise RuntimeError("No USB audio inputs found.")
|
||||
|
||||
TRANSPORT1 = 'serial:/dev/ttyAMA3,1000000,rtscts' # transport for raspberry pi gpio header
|
||||
TRANSPORT2 = 'serial:/dev/ttyAMA4,1000000,rtscts' # transport for raspberry pi gpio header
|
||||
|
||||
@@ -25,6 +25,16 @@ def _pa_like_hostapi_index():
|
||||
def _pw_dump():
|
||||
return json.loads(subprocess.check_output(["pw-dump"]))
|
||||
|
||||
def _sd_refresh():
|
||||
"""Force PortAudio to re-enumerate devices on next query.
|
||||
|
||||
sounddevice/PortAudio keeps a static device list after initialization.
|
||||
Terminating here ensures that subsequent sd.query_* calls re-initialize
|
||||
and see newly added devices (e.g., AES67 nodes created after start).
|
||||
"""
|
||||
sd._terminate() # private API, acceptable for runtime refresh
|
||||
sd._initialize()
|
||||
|
||||
def _sd_matches_from_names(pa_idx, names):
|
||||
names_l = {n.lower() for n in names if n}
|
||||
out = []
|
||||
@@ -41,6 +51,8 @@ def list_usb_pw_inputs():
|
||||
Return [(device_index, device_dict), ...] for PipeWire **input** nodes
|
||||
backed by **USB** devices (excludes monitor sources).
|
||||
"""
|
||||
# Refresh PortAudio so we see newly added nodes before mapping
|
||||
_sd_refresh()
|
||||
pa_idx = _pa_like_hostapi_index()
|
||||
pw = _pw_dump()
|
||||
|
||||
@@ -77,6 +89,8 @@ def list_network_pw_inputs():
|
||||
Return [(device_index, device_dict), ...] for PipeWire **input** nodes that
|
||||
look like network/AES67/RTP sources (excludes monitor sources).
|
||||
"""
|
||||
# Refresh PortAudio so we see newly added nodes before mapping
|
||||
_sd_refresh()
|
||||
pa_idx = _pa_like_hostapi_index()
|
||||
pw = _pw_dump()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user