use no pipewire for usb
This commit is contained in:
@@ -177,7 +177,7 @@ git checkout 9abe5fe7db729280080a0bbc1397a528cd3ce658
|
||||
rm -rf build
|
||||
cmake -S . -B build -G"Unix Makefiles" \
|
||||
-DBUILD_SHARED_LIBS=ON \
|
||||
-DPA_USE_ALSA=OFF \
|
||||
-DPA_USE_ALSA=ON \
|
||||
-DPA_USE_PULSEAUDIO=ON \
|
||||
-DPA_USE_JACK=OFF
|
||||
cmake --build build -j$(nproc)
|
||||
|
||||
@@ -53,6 +53,18 @@ from auracast.utils.read_lc3_file import read_lc3_file
|
||||
from auracast.utils.network_audio_receiver import NetworkAudioReceiverUncoded
|
||||
from auracast.utils.webrtc_audio_input import WebRTCAudioInput
|
||||
|
||||
# Configure tight ALSA latency via sounddevice defaults, if requested
|
||||
try:
|
||||
import os as _os
|
||||
import sounddevice as _sd # type: ignore
|
||||
_lat_ms = float(_os.environ.get('ALSA_LATENCY_MSEC', '') or '0')
|
||||
if _lat_ms > 0:
|
||||
_sec = _lat_ms / 1000.0
|
||||
# Apply to both input and output defaults; input is what we use
|
||||
_sd.default.latency = (_sec, _sec)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# Instantiate WebRTC audio input for streaming (can be used per-BIG or globally)
|
||||
|
||||
@@ -547,6 +559,21 @@ class Streamer():
|
||||
for attempt in range(1, max_attempts + 1):
|
||||
try:
|
||||
pcm_format = await audio_input.open()
|
||||
# Debug: report which backend/device was opened
|
||||
try:
|
||||
dev_index = None
|
||||
if isinstance(audio_source, str) and audio_source.startswith('device:'):
|
||||
try:
|
||||
dev_index = int(audio_source[7:])
|
||||
except Exception:
|
||||
dev_index = None
|
||||
if dev_index is None:
|
||||
dev_index = _sd.default.device[0]
|
||||
_dev = _sd.query_devices(dev_index)
|
||||
_host = _sd.query_hostapis()[_dev['hostapi']]['name']
|
||||
logging.info("Opened input device index=%s name=%s hostapi=%s", dev_index, _dev.get('name'), _host)
|
||||
except Exception:
|
||||
pass
|
||||
break # success
|
||||
except _sd.PortAudioError as err:
|
||||
# -9985 == paDeviceUnavailable
|
||||
@@ -666,6 +693,22 @@ class Streamer():
|
||||
|
||||
await big['iso_queue'].write(lc3_frame)
|
||||
|
||||
# Every 100 packets, if input is a SoundDevice stream, log available frames
|
||||
pkt_counter = big.get('pkt_counter', 0) + 1
|
||||
big['pkt_counter'] = pkt_counter
|
||||
if pkt_counter % 100 == 0:
|
||||
try:
|
||||
ai = big.get('audio_input')
|
||||
if ai is not None and hasattr(ai, '_stream') and getattr(ai, '_stream') is not None:
|
||||
sd_stream = getattr(ai, '_stream')
|
||||
avail = None
|
||||
# sounddevice streams expose read_available (frames)
|
||||
if hasattr(sd_stream, 'read_available'):
|
||||
avail = sd_stream.read_available
|
||||
logging.info('SD read_available (frames) after %d packets: %s', pkt_counter, avail)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if all(stream_finished): # Take into account that multiple files have different lengths
|
||||
logging.info('All streams finished, stopping streamer')
|
||||
self.is_streaming = False
|
||||
|
||||
@@ -60,18 +60,18 @@ if __name__ == "__main__":
|
||||
os.chdir(os.path.dirname(__file__))
|
||||
# Load .env located next to this script (only uppercase keys will be referenced)
|
||||
load_dotenv(dotenv_path='.env')
|
||||
# Default tight ALSA latency (ms); can be overridden via environment
|
||||
os.environ.setdefault('ALSA_LATENCY_MSEC', '2')
|
||||
|
||||
os.environ.setdefault("PULSE_LATENCY_MSEC", "3")
|
||||
|
||||
# Refresh device cache and list inputs
|
||||
refresh_pw_cache()
|
||||
usb_inputs = get_usb_pw_inputs()
|
||||
logging.info("USB pw inputs:")
|
||||
logging.info("USB ALSA inputs:")
|
||||
for i, d in usb_inputs:
|
||||
logging.info(f"{i}: {d['name']} in={d['max_input_channels']}")
|
||||
|
||||
aes67_inputs = get_network_pw_inputs()
|
||||
logging.info("AES67 pw inputs:")
|
||||
logging.info("AES67/Network ALSA inputs:")
|
||||
for i, d in aes67_inputs:
|
||||
logging.info(f"{i}: {d['name']} in={d['max_input_channels']}")
|
||||
|
||||
@@ -118,7 +118,7 @@ if __name__ == "__main__":
|
||||
|
||||
TRANSPORT1 = 'serial:/dev/ttyAMA3,1000000,rtscts' # transport for raspberry pi gpio header
|
||||
TRANSPORT2 = 'serial:/dev/ttyAMA4,1000000,rtscts' # transport for raspberry pi gpio header
|
||||
# Capture at 48 kHz to avoid PipeWire resampler latency; encode LC3 at 24 kHz
|
||||
# Capture at 48 kHz to avoid resampler latency; encode LC3 at 24 kHz
|
||||
CAPTURE_SRATE = 48000
|
||||
LC3_SRATE = 24000
|
||||
OCTETS_PER_FRAME=60
|
||||
@@ -157,7 +157,7 @@ if __name__ == "__main__":
|
||||
octets_per_frame = OCTETS_PER_FRAME,
|
||||
transport=TRANSPORT1
|
||||
)
|
||||
#config.debug = True
|
||||
config.debug = True
|
||||
|
||||
logging.info(config.model_dump_json(indent=2))
|
||||
multicast.run_async(
|
||||
|
||||
@@ -1,29 +1,12 @@
|
||||
import sounddevice as sd
|
||||
import os, re, json, subprocess
|
||||
import re
|
||||
|
||||
def devices_by_backend(backend_name: str):
|
||||
hostapis = sd.query_hostapis() # list of host APIs
|
||||
# find the host API index by (case-insensitive) name match
|
||||
try:
|
||||
hostapi_idx = next(
|
||||
i for i, ha in enumerate(hostapis)
|
||||
if backend_name.lower() in ha['name'].lower()
|
||||
)
|
||||
except StopIteration:
|
||||
raise ValueError(f"No host API matching {backend_name!r}. "
|
||||
f"Available: {[ha['name'] for ha in hostapis]}")
|
||||
# return (global_index, device_dict) pairs filtered by that host API
|
||||
return [(i, d) for i, d in enumerate(sd.query_devices())
|
||||
if d['hostapi'] == hostapi_idx]
|
||||
|
||||
def _pa_like_hostapi_index():
|
||||
def _alsa_hostapi_index():
|
||||
"""Return the PortAudio host API index for ALSA."""
|
||||
for i, ha in enumerate(sd.query_hostapis()):
|
||||
if any(k in ha["name"] for k in ("PipeWire", "PulseAudio")):
|
||||
if 'alsa' in ha['name'].lower():
|
||||
return i
|
||||
raise RuntimeError("PipeWire/PulseAudio host API not present in PortAudio.")
|
||||
|
||||
def _pw_dump():
|
||||
return json.loads(subprocess.check_output(["pw-dump"]))
|
||||
raise RuntimeError("ALSA host API not present in PortAudio.")
|
||||
|
||||
def _sd_refresh():
|
||||
"""Force PortAudio to re-enumerate devices on next query.
|
||||
@@ -35,98 +18,52 @@ def _sd_refresh():
|
||||
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 = []
|
||||
for i, d in enumerate(sd.query_devices()):
|
||||
if d["hostapi"] != pa_idx or d["max_input_channels"] <= 0:
|
||||
continue
|
||||
dn = d["name"].lower()
|
||||
# Exclude monitor devices (e.g., "Monitor of ...") to avoid false positives
|
||||
if "monitor" in dn:
|
||||
continue
|
||||
if any(n in dn for n in names_l):
|
||||
out.append((i, d))
|
||||
return out
|
||||
|
||||
# Module-level caches for device lists
|
||||
_usb_inputs_cache = []
|
||||
_network_inputs_cache = []
|
||||
|
||||
def get_usb_pw_inputs():
|
||||
"""Return cached list of USB PipeWire inputs."""
|
||||
"""Return cached list of USB ALSA inputs."""
|
||||
return _usb_inputs_cache
|
||||
|
||||
def get_network_pw_inputs():
|
||||
"""Return cached list of Network/AES67 PipeWire inputs."""
|
||||
"""Return cached list of non-USB ALSA inputs (used for AES67 selection heuristics)."""
|
||||
return _network_inputs_cache
|
||||
|
||||
def refresh_pw_cache():
|
||||
"""
|
||||
Performs a full device scan and updates the internal caches for both USB
|
||||
and Network audio devices. This is a heavy operation and should not be
|
||||
called frequently or during active streams.
|
||||
ALSA-only device scan using PortAudio via sounddevice. Updates internal
|
||||
caches for USB and non-USB input devices. Heuristic: device name contains
|
||||
'USB' -> USB; everything else -> non-USB.
|
||||
"""
|
||||
global _usb_inputs_cache, _network_inputs_cache
|
||||
|
||||
# Force PortAudio to re-enumerate devices
|
||||
_sd_refresh()
|
||||
pa_idx = _pa_like_hostapi_index()
|
||||
pw = _pw_dump()
|
||||
try:
|
||||
alsa_idx = _alsa_hostapi_index()
|
||||
except Exception:
|
||||
# Fallback to first host API if ALSA not found
|
||||
alsa_idx = 0
|
||||
|
||||
# --- Pass 1: Map device.id to device.bus ---
|
||||
device_bus = {}
|
||||
for obj in pw:
|
||||
if obj.get("type") == "PipeWire:Interface:Device":
|
||||
props = (obj.get("info") or {}).get("props") or {}
|
||||
device_bus[obj["id"]] = (props.get("device.bus") or "").lower()
|
||||
devices = sd.query_devices()
|
||||
alsa_inputs = [
|
||||
(i, d)
|
||||
for i, d in enumerate(devices)
|
||||
if d.get('hostapi') == alsa_idx and d.get('max_input_channels', 0) > 0
|
||||
]
|
||||
|
||||
# --- Pass 2: Identify all USB and Network nodes ---
|
||||
usb_input_names = set()
|
||||
network_input_names = set()
|
||||
for obj in pw:
|
||||
if obj.get("type") != "PipeWire:Interface:Node":
|
||||
continue
|
||||
props = (obj.get("info") or {}).get("props") or {}
|
||||
media = (props.get("media.class") or "").lower()
|
||||
if "source" not in media and "stream/input" not in media:
|
||||
usb, nonusb = [], []
|
||||
for i, d in alsa_inputs:
|
||||
name_l = str(d.get('name', '')).lower()
|
||||
if 'monitor' in name_l:
|
||||
continue
|
||||
if 'usb' in name_l or re.search(r'\busb\b', name_l):
|
||||
usb.append((i, d))
|
||||
else:
|
||||
nonusb.append((i, d))
|
||||
|
||||
nname = (props.get("node.name") or "")
|
||||
ndesc = (props.get("node.description") or "")
|
||||
# Skip all monitor sources
|
||||
if ".monitor" in nname.lower() or "monitor" in ndesc.lower():
|
||||
continue
|
||||
|
||||
# Check for USB
|
||||
bus = (props.get("device.bus") or device_bus.get(props.get("device.id")) or "").lower()
|
||||
if bus == "usb":
|
||||
usb_input_names.add(ndesc or nname)
|
||||
continue # A device is either USB or Network, not both
|
||||
|
||||
# Heuristics for Network/AES67/RTP
|
||||
text = (nname + " " + ndesc).lower()
|
||||
media_name = (props.get("media.name") or "").lower()
|
||||
node_group = (props.get("node.group") or "").lower()
|
||||
node_network_flag = bool(props.get("node.network"))
|
||||
has_rtp_keys = any(k in props for k in ("rtp.session", "rtp.source.ip"))
|
||||
has_sess_keys = any(k in props for k in ("sess.name", "sess.media"))
|
||||
|
||||
is_network = (
|
||||
bus == "network" or
|
||||
node_network_flag or
|
||||
"rtp" in media_name or
|
||||
any(k in text for k in ("rtp", "sap", "aes67", "network", "raop", "airplay")) or
|
||||
has_rtp_keys or
|
||||
has_sess_keys or
|
||||
("pipewire.ptp" in node_group)
|
||||
)
|
||||
if is_network:
|
||||
network_input_names.add(ndesc or nname)
|
||||
|
||||
# --- Final Step: Update caches ---
|
||||
_usb_inputs_cache = _sd_matches_from_names(pa_idx, usb_input_names)
|
||||
_network_inputs_cache = _sd_matches_from_names(pa_idx, network_input_names)
|
||||
_usb_inputs_cache = usb
|
||||
_network_inputs_cache = nonusb
|
||||
|
||||
|
||||
# Populate cache on initial module load
|
||||
|
||||
Reference in New Issue
Block a user