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:
@@ -5,19 +5,13 @@ multicast_script
|
||||
Loads environment variables from a .env file located next to this script
|
||||
and configures the multicast broadcast. Only UPPERCASE keys are read.
|
||||
|
||||
This version uses ALSA backend for USB audio devices (no PipeWire required).
|
||||
|
||||
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".
|
||||
|
||||
@@ -27,17 +21,13 @@ Environment variables
|
||||
- LANGUATE: ISO 639-3 language code used by config (intentional key name).
|
||||
Default: "deu".
|
||||
|
||||
- PULSE_LATENCY_MSEC: Pulse/PipeWire latency hint in milliseconds.
|
||||
Default: 3.
|
||||
|
||||
Examples (.env)
|
||||
---------------
|
||||
LOG_LEVEL=DEBUG
|
||||
INPUT=aes67,8f6326
|
||||
BROADCAST_NAME=MyBroadcast
|
||||
PROGRAM_INFO="Live announcements"
|
||||
LANGUATE=deu
|
||||
"""
|
||||
LANGUATE=deu"""
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
@@ -45,9 +35,10 @@ from dotenv import load_dotenv
|
||||
from auracast import multicast
|
||||
from auracast import auracast_config
|
||||
from auracast.utils.sounddevice_utils import (
|
||||
get_usb_pw_inputs,
|
||||
get_network_pw_inputs,
|
||||
get_alsa_usb_inputs,
|
||||
get_network_pw_inputs, # PipeWire network (AES67) inputs
|
||||
refresh_pw_cache,
|
||||
resolve_input_device_index,
|
||||
)
|
||||
|
||||
|
||||
@@ -60,65 +51,81 @@ 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')
|
||||
|
||||
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:")
|
||||
# Refresh PipeWire cache and list devices (Network only for PW; USB via ALSA)
|
||||
try:
|
||||
refresh_pw_cache()
|
||||
except Exception:
|
||||
pass
|
||||
pw_net = get_network_pw_inputs()
|
||||
# List PipeWire Network inputs
|
||||
logging.info("PipeWire Network inputs:")
|
||||
for i, d in pw_net:
|
||||
logging.info(f"{i}: {d['name']} in={d.get('max_input_channels', 0)}")
|
||||
|
||||
# Also list USB ALSA inputs (fallback path)
|
||||
usb_inputs = get_alsa_usb_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:")
|
||||
for i, d in aes67_inputs:
|
||||
logging.info(f"{i}: {d['name']} in={d['max_input_channels']}")
|
||||
|
||||
# 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)
|
||||
# Optional device selection via .env: match by string or substring (uppercase keys only)
|
||||
device_match = os.environ.get('INPUT_DEVICE')
|
||||
device_match = device_match.strip() if isinstance(device_match, str) else None
|
||||
|
||||
# Loop until a device becomes available (prefer PW Network, then ALSA USB)
|
||||
selected_dev = 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:
|
||||
refresh_pw_cache()
|
||||
current = get_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]
|
||||
selected_dev = sel[1]
|
||||
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:
|
||||
input_sel, selected_dev = aes67_inputs[0]
|
||||
logging.info(f"Selected first AES67 input: index={input_sel}, device={selected_dev['name']}")
|
||||
else:
|
||||
# Loop until a USB input becomes available (mirror AES67 retry behavior)
|
||||
while True:
|
||||
while True:
|
||||
try:
|
||||
refresh_pw_cache()
|
||||
current = get_usb_pw_inputs()
|
||||
if current:
|
||||
input_sel, selected_dev = current[0]
|
||||
logging.info(f"Selected first USB input: index={input_sel}, device={selected_dev['name']}")
|
||||
except Exception:
|
||||
pass
|
||||
pw_net = get_network_pw_inputs()
|
||||
# 1) Try to satisfy explicit .env match on PW Network
|
||||
if device_match and pw_net:
|
||||
for _, d in pw_net:
|
||||
name = d.get('name', '')
|
||||
if device_match in name:
|
||||
idx = resolve_input_device_index(name)
|
||||
if idx is not None:
|
||||
input_sel = idx
|
||||
selected_dev = d
|
||||
logging.info(f"Selected Network input by match '{device_match}' (PipeWire): index={input_sel}, device={name}")
|
||||
break
|
||||
if selected_dev is not None:
|
||||
break
|
||||
logging.info("Waiting for USB input... retrying in 2s")
|
||||
time.sleep(2)
|
||||
if pw_net and selected_dev is None:
|
||||
_, d0 = pw_net[0]
|
||||
idx = resolve_input_device_index(d0.get('name', ''))
|
||||
if idx is not None:
|
||||
input_sel = idx
|
||||
selected_dev = d0
|
||||
logging.info(f"Selected first Network input (PipeWire): index={input_sel}, device={d0['name']}")
|
||||
break
|
||||
current_alsa = get_alsa_usb_inputs()
|
||||
# 2) Try to satisfy explicit .env match on ALSA USB
|
||||
if device_match and current_alsa and selected_dev is None:
|
||||
matched = None
|
||||
for idx_alsa, d in current_alsa:
|
||||
name = d.get('name', '')
|
||||
if device_match in name:
|
||||
matched = (idx_alsa, d)
|
||||
break
|
||||
if matched is not None:
|
||||
input_sel, selected_dev = matched
|
||||
logging.info(f"Selected USB input by match '{device_match}' (ALSA): index={input_sel}, device={selected_dev['name']}")
|
||||
break
|
||||
# Fallback to first ALSA USB
|
||||
if current_alsa and selected_dev is None:
|
||||
input_sel, selected_dev = current_alsa[0]
|
||||
logging.info(f"Selected first USB input (ALSA): index={input_sel}, device={selected_dev['name']}")
|
||||
break
|
||||
logging.info("Waiting for audio input (prefer PW Network, then ALSA USB)... retrying in 2s")
|
||||
time.sleep(2)
|
||||
|
||||
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
|
||||
@@ -150,14 +157,16 @@ if __name__ == "__main__":
|
||||
),
|
||||
#auracast_config.AuracastBigConfigEng(),
|
||||
],
|
||||
immediate_rendering=True,
|
||||
immediate_rendering=False,
|
||||
presentation_delay_us=40000,
|
||||
qos_config=auracast_config.AuracastQosHigh(),
|
||||
auracast_sampling_rate_hz = LC3_SRATE,
|
||||
octets_per_frame = OCTETS_PER_FRAME,
|
||||
transport=TRANSPORT1
|
||||
transport=TRANSPORT1,
|
||||
enable_adaptive_frame_dropping=True,
|
||||
)
|
||||
#config.debug = True
|
||||
config.debug = False
|
||||
|
||||
|
||||
logging.info(config.model_dump_json(indent=2))
|
||||
multicast.run_async(
|
||||
|
||||
Reference in New Issue
Block a user