Implement adaptive frame dropping (#10)
- Implement adaptive frame dropping to prevent latency from accumulating - small packets are dropped and a crossfade is used to hide the dropping. - still audible in some situations Co-authored-by: pstruebi <struebin.patrick.com> Reviewed-on: https://gitea.pstruebi.xyz/auracaster/bumble-auracast/pulls/10
This commit was merged in pull request #10.
This commit is contained in:
@@ -170,11 +170,29 @@ if audio_mode == "Demo":
|
||||
"6 × 16kHz": {"quality": "Fair (16kHz)", "streams": 6},
|
||||
}
|
||||
demo_options = list(demo_stream_map.keys())
|
||||
default_demo = demo_options[0]
|
||||
default_index = 0
|
||||
saved_type = saved_settings.get('demo_stream_type')
|
||||
if isinstance(saved_type, str) and saved_type in demo_options:
|
||||
default_index = demo_options.index(saved_type)
|
||||
else:
|
||||
saved_rate = saved_settings.get('auracast_sampling_rate_hz')
|
||||
saved_total = saved_settings.get('demo_total_streams')
|
||||
if saved_total is None and saved_settings.get('audio_mode') == 'Demo':
|
||||
saved_total = len(saved_settings.get('channel_names') or [])
|
||||
try:
|
||||
if saved_rate and saved_total:
|
||||
for i, label in enumerate(demo_options):
|
||||
cfg = demo_stream_map[label]
|
||||
rate_for_label = QUALITY_MAP[cfg['quality']]['rate']
|
||||
if cfg['streams'] == int(saved_total) and int(saved_rate) == rate_for_label:
|
||||
default_index = i
|
||||
break
|
||||
except Exception:
|
||||
default_index = 0
|
||||
demo_selected = st.selectbox(
|
||||
"Demo Stream Type",
|
||||
demo_options,
|
||||
index=0,
|
||||
index=default_index,
|
||||
help="Select the demo stream configuration."
|
||||
)
|
||||
# Stream password and flags (same as USB/AES67)
|
||||
|
||||
@@ -19,8 +19,9 @@ from auracast import multicast_control, auracast_config
|
||||
import sounddevice as sd # type: ignore
|
||||
import traceback
|
||||
from auracast.utils.sounddevice_utils import (
|
||||
get_usb_pw_inputs,
|
||||
get_network_pw_inputs,
|
||||
get_alsa_usb_inputs,
|
||||
resolve_input_device_index,
|
||||
refresh_pw_cache,
|
||||
)
|
||||
from auracast.utils.reset_utils import reset_nrf54l
|
||||
@@ -32,8 +33,6 @@ STREAM_SETTINGS_FILE = os.path.join(os.path.dirname(__file__), 'stream_settings.
|
||||
TRANSPORT1 = os.getenv('TRANSPORT1', 'serial:/dev/ttyAMA3,1000000,rtscts') # transport for raspberry pi gpio header
|
||||
TRANSPORT2 = os.getenv('TRANSPORT2', 'serial:/dev/ttyAMA4,1000000,rtscts') # transport for raspberry pi gpio header
|
||||
|
||||
|
||||
|
||||
os.environ["PULSE_LATENCY_MSEC"] = "3"
|
||||
|
||||
# In-memory cache to avoid disk I/O on hot paths like /status
|
||||
@@ -105,7 +104,7 @@ app.add_middleware(
|
||||
# Initialize global configuration
|
||||
global_config_group = auracast_config.AuracastConfigGroup()
|
||||
|
||||
class StreamerWorker:
|
||||
class StreamerWorker: # TODO: is wraping in this Worker stricly nececcarry ?
|
||||
"""Owns multicaster(s) on a dedicated asyncio loop in a background thread."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
@@ -162,7 +161,16 @@ class StreamerWorker:
|
||||
pass
|
||||
self._multicaster1 = None
|
||||
|
||||
# overwrite some configurations
|
||||
conf.transport = TRANSPORT1
|
||||
# Enable adaptive frame dropping only for device-based inputs (not file/demo)
|
||||
try:
|
||||
conf.enable_adaptive_frame_dropping = any(
|
||||
isinstance(big.audio_source, str) and big.audio_source.startswith('device:')
|
||||
for big in conf.bigs
|
||||
)
|
||||
except Exception:
|
||||
conf.enable_adaptive_frame_dropping = False
|
||||
# Derive device name and input mode
|
||||
first_source = conf.bigs[0].audio_source if conf.bigs else ''
|
||||
input_device_name = None
|
||||
@@ -170,21 +178,28 @@ class StreamerWorker:
|
||||
if first_source.startswith('device:'):
|
||||
input_device_name = first_source.split(':', 1)[1] if ':' in first_source else None
|
||||
try:
|
||||
usb_names = {d.get('name') for _, d in get_usb_pw_inputs()}
|
||||
alsa_usb_names = {d.get('name') for _, d in get_alsa_usb_inputs()}
|
||||
except Exception:
|
||||
alsa_usb_names = set()
|
||||
try:
|
||||
net_names = {d.get('name') for _, d in get_network_pw_inputs()}
|
||||
except Exception:
|
||||
usb_names, net_names = set(), set()
|
||||
audio_mode_persist = 'Network' if (input_device_name in net_names) else 'USB'
|
||||
|
||||
# Map device name to index and configure input_format
|
||||
device_index = int(input_device_name) if (input_device_name and input_device_name.isdigit()) else get_device_index_by_name(input_device_name or '')
|
||||
# Map device name to index using centralized resolver
|
||||
if input_device_name and input_device_name.isdigit():
|
||||
device_index = int(input_device_name)
|
||||
else:
|
||||
device_index = resolve_input_device_index(input_device_name or '')
|
||||
if device_index is None:
|
||||
raise HTTPException(status_code=400, detail=f"Audio device '{input_device_name}' not found.")
|
||||
for big in conf.bigs:
|
||||
if big.audio_source.startswith('device:'):
|
||||
big.audio_source = f'device:{device_index}'
|
||||
devinfo = sd.query_devices(device_index)
|
||||
capture_rate = int(devinfo.get('default_samplerate') or 48000)
|
||||
# Force capture at 48 kHz to avoid resampler latency and 44.1 kHz incompatibilities
|
||||
capture_rate = 48000
|
||||
max_in = int(devinfo.get('max_input_channels') or 1)
|
||||
channels = max(1, min(2, max_in))
|
||||
for big in conf.bigs:
|
||||
@@ -204,6 +219,14 @@ class StreamerWorker:
|
||||
auto_started = True
|
||||
|
||||
# Return proposed settings to persist on API side
|
||||
demo_count = sum(1 for big in conf.bigs if isinstance(big.audio_source, str) and big.audio_source.startswith('file:'))
|
||||
demo_rate = int(conf.auracast_sampling_rate_hz or 0)
|
||||
demo_type = None
|
||||
if demo_count > 0 and demo_rate > 0:
|
||||
if demo_rate in (48000, 24000, 16000):
|
||||
demo_type = f"{demo_count} × {demo_rate//1000}kHz"
|
||||
else:
|
||||
demo_type = f"{demo_count} × {demo_rate}Hz"
|
||||
return {
|
||||
'channel_names': [big.name for big in conf.bigs],
|
||||
'languages': [big.language for big in conf.bigs],
|
||||
@@ -218,6 +241,8 @@ class StreamerWorker:
|
||||
'immediate_rendering': getattr(conf, 'immediate_rendering', False),
|
||||
'assisted_listening_stream': getattr(conf, 'assisted_listening_stream', False),
|
||||
'stream_password': (conf.bigs[0].code if conf.bigs and getattr(conf.bigs[0], 'code', None) else None),
|
||||
'demo_total_streams': demo_count,
|
||||
'demo_stream_type': demo_type,
|
||||
'is_streaming': auto_started,
|
||||
}
|
||||
|
||||
@@ -230,10 +255,27 @@ class StreamerWorker:
|
||||
self._multicaster2 = None
|
||||
|
||||
conf.transport = TRANSPORT2
|
||||
# Enable adaptive frame dropping only for device-based inputs (not file/demo)
|
||||
try:
|
||||
conf.enable_adaptive_frame_dropping = any(
|
||||
isinstance(big.audio_source, str) and big.audio_source.startswith('device:')
|
||||
for big in conf.bigs
|
||||
)
|
||||
except Exception:
|
||||
conf.enable_adaptive_frame_dropping = False
|
||||
for big in conf.bigs:
|
||||
if big.audio_source.startswith('device:'):
|
||||
device_name = big.audio_source.split(':', 1)[1]
|
||||
device_index = get_device_index_by_name(device_name)
|
||||
# Resolve backend preference by membership
|
||||
try:
|
||||
net_names = {d.get('name') for _, d in get_network_pw_inputs()}
|
||||
except Exception:
|
||||
net_names = set()
|
||||
try:
|
||||
alsa_usb_names = {d.get('name') for _, d in get_alsa_usb_inputs()}
|
||||
except Exception:
|
||||
alsa_usb_names = set()
|
||||
device_index = resolve_input_device_index(device_name)
|
||||
if device_index is None:
|
||||
raise HTTPException(status_code=400, detail=f"Audio device '{device_name}' not found.")
|
||||
big.audio_source = f'device:{device_index}'
|
||||
@@ -313,6 +355,24 @@ async def initialize2(conf: auracast_config.AuracastConfigGroup):
|
||||
try:
|
||||
log.info('Initializing multicaster2 with config:\n %s', conf.model_dump_json(indent=2))
|
||||
await streamer.call(streamer._w_init_secondary, conf)
|
||||
try:
|
||||
is_demo = any(isinstance(big.audio_source, str) and big.audio_source.startswith('file:') for big in conf.bigs)
|
||||
if is_demo:
|
||||
settings = load_stream_settings() or {}
|
||||
primary_count = int(settings.get('demo_total_streams') or len(settings.get('channel_names') or []))
|
||||
secondary_count = len(conf.bigs or [])
|
||||
total = primary_count + secondary_count
|
||||
settings['demo_total_streams'] = total
|
||||
demo_rate = int(conf.auracast_sampling_rate_hz or 0)
|
||||
if demo_rate > 0:
|
||||
if demo_rate in (48000, 24000, 16000):
|
||||
settings['demo_stream_type'] = f"{total} × {demo_rate//1000}kHz"
|
||||
else:
|
||||
settings['demo_stream_type'] = f"{total} × {demo_rate}Hz"
|
||||
settings['timestamp'] = datetime.utcnow().isoformat()
|
||||
save_stream_settings(settings)
|
||||
except Exception:
|
||||
log.warning("Failed to persist demo_total_streams in /init2", exc_info=True)
|
||||
except Exception as e:
|
||||
log.error("Exception in /init2: %s", traceback.format_exc())
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@@ -415,7 +475,7 @@ async def _autostart_from_settings():
|
||||
):
|
||||
return
|
||||
# Check against the cached device lists
|
||||
usb = [d for _, d in get_usb_pw_inputs()]
|
||||
usb = [d for _, d in get_alsa_usb_inputs()]
|
||||
net = [d for _, d in get_network_pw_inputs()]
|
||||
names = {d.get('name') for d in usb} | {d.get('name') for d in net}
|
||||
if input_device_name in names:
|
||||
@@ -471,11 +531,11 @@ async def _startup_autostart_event():
|
||||
|
||||
@app.get("/audio_inputs_pw_usb")
|
||||
async def audio_inputs_pw_usb():
|
||||
"""List PipeWire USB input nodes from cache."""
|
||||
"""List USB input devices using ALSA backend (USB is ALSA in our scheme)."""
|
||||
try:
|
||||
devices = [
|
||||
{"id": idx, "name": dev.get("name"), "max_input_channels": dev.get("max_input_channels", 0)}
|
||||
for idx, dev in get_usb_pw_inputs()
|
||||
for idx, dev in get_alsa_usb_inputs()
|
||||
]
|
||||
return {"inputs": devices}
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user