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 logging
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from auracast import multicast
|
from auracast import multicast
|
||||||
from auracast import auracast_config
|
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("AURACAST_SD_LATENCY", "0.0027") # ~128/48000 s
|
||||||
os.environ.setdefault("PULSE_LATENCY_MSEC", "1")
|
os.environ.setdefault("PULSE_LATENCY_MSEC", "1")
|
||||||
os.environ.setdefault("PIPEWIRE_LATENCY", "128/48000")
|
os.environ.setdefault("PIPEWIRE_LATENCY", "128/48000")
|
||||||
logging.info("USB pw inputs:")
|
|
||||||
usb_inputs = list_usb_pw_inputs()
|
usb_inputs = list_usb_pw_inputs()
|
||||||
logging.info("AEs67 pw inputs:")
|
logging.info("USB pw inputs:")
|
||||||
aes67_inputs = list_network_pw_inputs()
|
|
||||||
for i, d in usb_inputs:
|
for i, d in usb_inputs:
|
||||||
logging.info(f"{i}: {d['name']} in={d['max_input_channels']}")
|
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:
|
for i, d in aes67_inputs:
|
||||||
logging.info(f"{i}: {d['name']} in={d['max_input_channels']}")
|
logging.info(f"{i}: {d['name']} in={d['max_input_channels']}")
|
||||||
|
|
||||||
# Input selection (usb | network). Default to usb.
|
# Input selection (usb | aes67). Default to usb.
|
||||||
input_mode = (os.environ.get('INPUT', 'usb') or 'usb').lower()
|
# Allows specifying an AES67 device by substring: INPUT=aes67,<substring>
|
||||||
if input_mode == 'network':
|
# Example: INPUT=aes67,8f6326 will match a device name containing "8f6326".
|
||||||
if aes67_inputs:
|
input_env = os.environ.get('INPUT', 'usb') or 'usb'
|
||||||
input_sel = aes67_inputs[0][0]
|
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:
|
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:
|
else:
|
||||||
if usb_inputs:
|
if usb_inputs:
|
||||||
input_sel = usb_inputs[0][0]
|
input_sel = usb_inputs[0][0]
|
||||||
|
logging.info(f"Selected first USB input: index={input_sel}, device={usb_inputs[0][1]['name']}")
|
||||||
else:
|
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
|
TRANSPORT1 = 'serial:/dev/ttyAMA3,1000000,rtscts' # transport for raspberry pi gpio header
|
||||||
TRANSPORT2 = 'serial:/dev/ttyAMA4,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():
|
def _pw_dump():
|
||||||
return json.loads(subprocess.check_output(["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):
|
def _sd_matches_from_names(pa_idx, names):
|
||||||
names_l = {n.lower() for n in names if n}
|
names_l = {n.lower() for n in names if n}
|
||||||
out = []
|
out = []
|
||||||
@@ -41,6 +51,8 @@ def list_usb_pw_inputs():
|
|||||||
Return [(device_index, device_dict), ...] for PipeWire **input** nodes
|
Return [(device_index, device_dict), ...] for PipeWire **input** nodes
|
||||||
backed by **USB** devices (excludes monitor sources).
|
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()
|
pa_idx = _pa_like_hostapi_index()
|
||||||
pw = _pw_dump()
|
pw = _pw_dump()
|
||||||
|
|
||||||
@@ -77,6 +89,8 @@ def list_network_pw_inputs():
|
|||||||
Return [(device_index, device_dict), ...] for PipeWire **input** nodes that
|
Return [(device_index, device_dict), ...] for PipeWire **input** nodes that
|
||||||
look like network/AES67/RTP sources (excludes monitor sources).
|
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()
|
pa_idx = _pa_like_hostapi_index()
|
||||||
pw = _pw_dump()
|
pw = _pw_dump()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user