2 channels per radio are working, 3 are shaky. UI is now robust.

This commit is contained in:
pober
2026-01-13 14:54:13 +01:00
committed by pstruebi
parent 921dd93c64
commit 06b18914f0
2 changed files with 663 additions and 108 deletions
+622 -100
View File
@@ -111,6 +111,20 @@ is_streaming = bool(saved_settings.get("is_streaming", False))
secondary_status = saved_settings.get("secondary") or {}
secondary_is_streaming = bool(saved_settings.get("secondary_is_streaming", secondary_status.get("is_streaming", False)))
def validate_unique_input_devices(radio1_streams, radio2_streams):
"""Validate that input devices are unique across all streams within each radio."""
# Check Radio 1 devices
r1_devices = [s['input_device'] for s in radio1_streams if s.get('input_device')]
if len(r1_devices) != len(set(r1_devices)):
return False, "Duplicate input devices detected in Radio 1 streams. Each stream must use a unique input device."
# Check Radio 2 devices
r2_devices = [s['input_device'] for s in radio2_streams if s.get('input_device')]
if len(r2_devices) != len(set(r2_devices)):
return False, "Duplicate input devices detected in Radio 2 streams. Each stream must use a unique input device."
return True, ""
st.title("Auracast Audio Mode Control")
def render_stream_controls(status_streaming: bool, start_label: str, stop_label: str, mode_label: str):
@@ -174,6 +188,15 @@ if audio_mode == "Analog":
else:
running_mode = backend_mode_mapped if (is_streaming and backend_mode_mapped) else audio_mode
# Start/Stop buttons and status (moved to top)
if audio_mode != "Demo":
start_stream, stop_stream = render_stream_controls(is_streaming, "Start Auracast", "Stop Auracast", running_mode)
else:
start_stream, stop_stream = False, False
# Placeholder for validation errors (will be filled in later)
validation_error_placeholder = st.empty()
is_started = False
is_stopped = False
@@ -368,6 +391,11 @@ else:
# --- Radio 1 controls ---
st.subheader("Radio 1")
# Use analog-specific defaults (not from saved settings which may have Dante values)
default_name = "Analog_Radio_1"
default_program_info = "Analog Radio Broadcast"
default_lang = "deu"
quality_options = list(QUALITY_MAP.keys())
default_quality = "Medium (24kHz)" if "Medium (24kHz)" in quality_options else quality_options[0]
quality1 = st.selectbox(
@@ -492,6 +520,11 @@ else:
)
if radio2_enabled:
# Use analog-specific defaults for Radio 2
default_name_r2 = "Analog_Radio_2"
default_program_info_r2 = "Analog Radio Broadcast"
default_lang_r2 = "deu"
quality2 = st.selectbox(
"Stream Quality (Radio 2)",
quality_options,
@@ -537,18 +570,18 @@ else:
with col_r2_name:
stream_name2 = st.text_input(
"Channel Name (Radio 2)",
value=f"{default_name}_2",
value=default_name_r2,
help="Name for the second analog radio (Radio 2)."
)
with col_r2_lang:
language2 = st.text_input(
"Language (ISO 639-3) (Radio 2)",
value=default_lang,
value=default_lang_r2,
help="Language code for Radio 2."
)
program_info2 = st.text_input(
"Program Info (Radio 2)",
value=default_program_info,
value=default_program_info_r2,
help="Program information for Radio 2."
)
@@ -600,7 +633,437 @@ else:
'qos_preset': qos_preset1,
}
else:
if audio_mode == "Network - Dante":
# --- Network - Dante mode with Radio 1 and Radio 2 categories ---
# Define stream configuration options
dante_stream_options = {
"1 × 48kHz": {"streams": 1, "quality": "High (48kHz)"},
"2 × 24kHz": {"streams": 2, "quality": "Medium (24kHz)"},
"3 × 16kHz": {"streams": 3, "quality": "Fair (16kHz)"}
}
# Get available Dante devices
if not is_streaming:
try:
resp = requests.get(f"{BACKEND_URL}/audio_inputs_dante")
device_list = resp.json().get('inputs', [])
except Exception as e:
st.error(f"Failed to fetch Dante devices: {e}")
device_list = []
input_options = [f"{d['name']} [{d['id']}]" for d in device_list]
option_name_map = {f"{d['name']} [{d['id']}]": d['name'] for d in device_list}
device_names = [d['name'] for d in device_list]
else:
input_options = []
option_name_map = {}
device_names = []
# --- Radio 1 Section ---
st.subheader("Radio 1")
# Stream count dropdown for Radio 1
r1_stream_options = list(dante_stream_options.keys())
saved_r1_config = saved_settings.get('dante_radio1', {})
saved_r1_streams = saved_r1_config.get('stream_config', '1x48')
default_r1_idx = r1_stream_options.index(saved_r1_streams) if saved_r1_streams in r1_stream_options else 0
r1_stream_config = st.selectbox(
"Stream Configuration (Radio 1)",
r1_stream_options,
index=default_r1_idx,
help="Select the number and quality of streams for Radio 1"
)
r1_num_streams = dante_stream_options[r1_stream_config]["streams"]
r1_quality = dante_stream_options[r1_stream_config]["quality"]
# Stream quality (moved directly under stream configuration)
r1_max_quality = r1_quality
r1_available_qualities = []
for quality in ["High (48kHz)", "Good (32kHz)", "Medium (24kHz)", "Fair (16kHz)"]:
# Check if this quality is equal to or lower than the max
if (r1_max_quality == "High (48kHz)" or
(r1_max_quality == "Medium (24kHz)" and quality in ["Medium (24kHz)", "Fair (16kHz)"]) or
(r1_max_quality == "Fair (16kHz)" and quality == "Fair (16kHz)")):
r1_available_qualities.append(quality)
saved_r1_quality = saved_r1_config.get('radio_quality', r1_max_quality)
if saved_r1_quality not in r1_available_qualities:
saved_r1_quality = r1_max_quality
r1_radio_quality = st.selectbox(
"Stream Quality (Radio 1)",
r1_available_qualities,
index=r1_available_qualities.index(saved_r1_quality),
help=f"Select stream quality for Radio 1. Maximum quality based on configuration: {r1_max_quality}"
)
# Radio-level settings for Radio 1
# First row: Assistive listening, immediate rendering, presentation delay, QoS
col_r1_flags1, col_r1_flags2, col_r1_pdelay, col_r1_qos = st.columns([1, 1, 0.7, 0.6], gap="small")
with col_r1_flags1:
r1_assisted_listening = st.checkbox(
"Assistive (R1)",
value=bool(saved_r1_config.get('assisted_listening', False)),
help="Assistive listening stream"
)
with col_r1_flags2:
r1_immediate_rendering = st.checkbox(
"Immediate (R1)",
value=bool(saved_r1_config.get('immediate_rendering', False)),
help="Ignore presentation delay"
)
with col_r1_pdelay:
default_pdelay = int(saved_r1_config.get('presentation_delay_us', 40000) or 40000)
default_pdelay_ms = max(10, min(200, default_pdelay // 1000))
r1_presentation_delay_ms = st.number_input(
"Delay (ms, R1)",
min_value=10, max_value=200, step=5, value=default_pdelay_ms,
help="Presentation delay for Radio 1"
)
with col_r1_qos:
qos_options = list(QOS_PRESET_MAP.keys())
saved_qos = saved_r1_config.get('qos_preset', 'Fast')
default_qos_idx = qos_options.index(saved_qos) if saved_qos in qos_options else 0
r1_qos_preset = st.selectbox(
"QoS (R1)", options=qos_options, index=default_qos_idx,
help="Quality of Service preset for Radio 1"
)
# Per-stream configuration for Radio 1
st.write("**Stream Configuration (Radio 1)**")
r1_streams = []
for i in range(r1_num_streams):
with st.expander(f"Stream {i+1} - Radio 1", expanded=True):
saved_streams = saved_r1_config.get('streams', [])
saved_stream = saved_streams[i] if i < len(saved_streams) else {}
# First row: Channel name and language
col_name, col_lang = st.columns([2, 1])
with col_name:
stream_name = st.text_input(
f"Channel Name",
value=saved_stream.get('name', f'Dante_R1_S{i+1}'),
key=f"r1_stream_{i}_name"
)
with col_lang:
stream_password = st.text_input(
f"Stream Password",
value=saved_stream.get('stream_password', ''),
type="password",
key=f"r1_stream_{i}_password",
help="Optional: Set a broadcast code for this stream"
)
# Second row: Program info and language
col_prog, col_lang_code = st.columns([2, 1])
with col_prog:
program_info = st.text_input(
f"Program Info",
value=saved_stream.get('program_info', f'Dante Radio 1 Stream {i+1}'),
key=f"r1_stream_{i}_program"
)
with col_lang_code:
language = st.text_input(
f"Language",
value=saved_stream.get('language', 'eng'),
key=f"r1_stream_{i}_lang",
help="ISO 639-3 language code"
)
# Third row: Input device
col_device = st.columns([1])[0]
with col_device:
# Session state key for persisting the selection
device_session_key = f"r1_stream_{i}_device_saved"
if not is_streaming and input_options:
# Get default from session state first, then from saved settings
default_input_name = st.session_state.get(device_session_key, saved_stream.get('input_device'))
default_input_label = None
for label, name in option_name_map.items():
if name == default_input_name:
default_input_label = label
break
if default_input_label not in input_options and input_options:
default_input_label = input_options[0]
selected_option = st.selectbox(
f"Input Device",
input_options,
index=input_options.index(default_input_label) if default_input_label in input_options else 0,
key=f"r1_stream_{i}_device"
)
input_device = option_name_map.get(selected_option)
# Save to session state for persistence
st.session_state[device_session_key] = input_device
else:
# When streaming, get the device from session state
current_device = st.session_state.get(device_session_key, saved_stream.get('input_device', 'No device'))
# Convert internal name to display label
display_label = current_device
for label, name in option_name_map.items():
if name == current_device:
display_label = label
break
st.selectbox(
f"Input Device",
[display_label if display_label else 'No device'],
index=0,
disabled=True,
key=f"r1_stream_{i}_device_disabled"
)
input_device = current_device
r1_streams.append({
'name': stream_name,
'program_info': program_info,
'language': language,
'input_device': input_device,
'stream_password': stream_password
})
# --- Radio 2 Section ---
st.subheader("Radio 2")
# Stream count dropdown for Radio 2 (includes "off" option)
r2_stream_options = ["off"] + r1_stream_options
saved_r2_config = saved_settings.get('dante_radio2', {})
saved_r2_streams = saved_r2_config.get('stream_config', 'off')
default_r2_idx = r2_stream_options.index(saved_r2_streams) if saved_r2_streams in r2_stream_options else 0
r2_stream_config = st.selectbox(
"Stream Configuration (Radio 2)",
r2_stream_options,
index=default_r2_idx,
help="Select the number and quality of streams for Radio 2"
)
radio2_enabled = r2_stream_config != "off"
if radio2_enabled:
r2_num_streams = dante_stream_options[r2_stream_config]["streams"]
r2_quality = dante_stream_options[r2_stream_config]["quality"]
# Stream quality (moved directly under stream configuration)
r2_max_quality = r2_quality
r2_available_qualities = []
for quality in ["High (48kHz)", "Good (32kHz)", "Medium (24kHz)", "Fair (16kHz)"]:
# Check if this quality is equal to or lower than the max
if (r2_max_quality == "High (48kHz)" or
(r2_max_quality == "Medium (24kHz)" and quality in ["Medium (24kHz)", "Fair (16kHz)"]) or
(r2_max_quality == "Fair (16kHz)" and quality == "Fair (16kHz)")):
r2_available_qualities.append(quality)
saved_r2_quality = saved_r2_config.get('radio_quality', r2_max_quality)
if saved_r2_quality not in r2_available_qualities:
saved_r2_quality = r2_max_quality
r2_radio_quality = st.selectbox(
"Stream Quality (Radio 2)",
r2_available_qualities,
index=r2_available_qualities.index(saved_r2_quality),
help=f"Select stream quality for Radio 2. Maximum quality based on configuration: {r2_max_quality}"
)
# Radio-level settings for Radio 2
# First row: Assistive listening, immediate rendering, presentation delay, QoS
col_r2_flags1, col_r2_flags2, col_r2_pdelay, col_r2_qos = st.columns([1, 1, 0.7, 0.6], gap="small")
with col_r2_flags1:
r2_assisted_listening = st.checkbox(
"Assistive (R2)",
value=bool(saved_r2_config.get('assisted_listening', False)),
help="Assistive listening stream"
)
with col_r2_flags2:
r2_immediate_rendering = st.checkbox(
"Immediate (R2)",
value=bool(saved_r2_config.get('immediate_rendering', False)),
help="Ignore presentation delay"
)
with col_r2_pdelay:
default_pdelay = int(saved_r2_config.get('presentation_delay_us', 40000) or 40000)
default_pdelay_ms = max(10, min(200, default_pdelay // 1000))
r2_presentation_delay_ms = st.number_input(
"Delay (ms, R2)",
min_value=10, max_value=200, step=5, value=default_pdelay_ms,
help="Presentation delay for Radio 2"
)
with col_r2_qos:
qos_options = list(QOS_PRESET_MAP.keys())
saved_qos = saved_r2_config.get('qos_preset', 'Fast')
default_qos_idx = qos_options.index(saved_qos) if saved_qos in qos_options else 0
r2_qos_preset = st.selectbox(
"QoS (R2)", options=qos_options, index=default_qos_idx,
help="Quality of Service preset for Radio 2"
)
# Per-stream configuration for Radio 2
st.write("**Stream Configuration (Radio 2)**")
r2_streams = []
for i in range(r2_num_streams):
with st.expander(f"Stream {i+1} - Radio 2", expanded=True):
saved_streams = saved_r2_config.get('streams', [])
saved_stream = saved_streams[i] if i < len(saved_streams) else {}
# First row: Channel name and password
col_name, col_pwd = st.columns([2, 1])
with col_name:
stream_name = st.text_input(
f"Channel Name",
value=saved_stream.get('name', f'Dante_R2_S{i+1}'),
key=f"r2_stream_{i}_name"
)
with col_pwd:
stream_password = st.text_input(
f"Stream Password",
value=saved_stream.get('stream_password', ''),
type="password",
key=f"r2_stream_{i}_password",
help="Optional: Set a broadcast code for this stream"
)
# Second row: Program info and language
col_prog, col_lang = st.columns([2, 1])
with col_prog:
program_info = st.text_input(
f"Program Info",
value=saved_stream.get('program_info', f'Dante Radio 2 Stream {i+1}'),
key=f"r2_stream_{i}_program"
)
with col_lang:
language = st.text_input(
f"Language",
value=saved_stream.get('language', 'eng'),
key=f"r2_stream_{i}_lang",
help="ISO 639-3 language code"
)
# Third row: Input device
col_device = st.columns([1])[0]
with col_device:
# Session state key for persisting the selection
device_session_key = f"r2_stream_{i}_device_saved"
if not is_streaming and input_options:
# Get default from session state first, then from saved settings
default_input_name = st.session_state.get(device_session_key, saved_stream.get('input_device'))
default_input_label = None
for label, name in option_name_map.items():
if name == default_input_name:
default_input_label = label
break
if default_input_label not in input_options and input_options:
default_input_label = input_options[0]
selected_option = st.selectbox(
f"Input Device",
input_options,
index=input_options.index(default_input_label) if default_input_label in input_options else 0,
key=f"r2_stream_{i}_device"
)
input_device = option_name_map.get(selected_option)
# Save to session state for persistence
st.session_state[device_session_key] = input_device
else:
# When streaming, get the device from session state
current_device = st.session_state.get(device_session_key, saved_stream.get('input_device', 'No device'))
# Convert internal name to display label
display_label = current_device
for label, name in option_name_map.items():
if name == current_device:
display_label = label
break
st.selectbox(
f"Input Device",
[display_label if display_label else 'No device'],
index=0,
disabled=True,
key=f"r2_stream_{i}_device_disabled"
)
input_device = current_device
r2_streams.append({
'name': stream_name,
'program_info': program_info,
'language': language,
'input_device': input_device,
'stream_password': stream_password
})
else:
r2_streams = []
# Validate unique input devices for Network - Dante mode
if audio_mode == "Network - Dante":
is_valid, error_msg = validate_unique_input_devices(r1_streams, r2_streams)
if not is_valid:
# Display error in the placeholder at the top
validation_error_placeholder.error(error_msg)
# Show device refresh button if no devices found
if not input_options and not is_streaming:
st.warning("No Dante inputs found. Check asound.conf configuration.")
if st.button("Refresh", key="refresh_dante_devices", disabled=is_streaming):
try:
r = requests.post(f"{BACKEND_URL}/refresh_audio_devices", timeout=8)
if not r.ok:
st.error(f"Failed to refresh: {r.text}")
except Exception as e:
st.error(f"Failed to refresh devices: {e}")
st.rerun()
# Store radio configurations for backend
radio1_cfg = {
'stream_config': r1_stream_config,
'max_quality': r1_quality, # Original max quality from stream config
'radio_quality': r1_radio_quality, # User-selected quality (may be lower)
'quality': r1_radio_quality, # Use selected quality for backend
'streams': r1_streams,
'assisted_listening': r1_assisted_listening,
'immediate_rendering': r1_immediate_rendering,
'presentation_delay_ms': r1_presentation_delay_ms,
'qos_preset': r1_qos_preset,
}
radio2_cfg = {
'stream_config': r2_stream_config,
'max_quality': r2_quality if radio2_enabled else None, # Original max quality from stream config
'radio_quality': r2_radio_quality if radio2_enabled else None, # User-selected quality
'quality': r2_radio_quality if radio2_enabled else None, # Use selected quality for backend
'streams': r2_streams,
'assisted_listening': r2_assisted_listening if radio2_enabled else False,
'immediate_rendering': r2_immediate_rendering if radio2_enabled else False,
'presentation_delay_ms': r2_presentation_delay_ms if radio2_enabled else 40000,
'qos_preset': r2_qos_preset if radio2_enabled else 'Fast',
} if radio2_enabled else None
if audio_mode in ("USB", "Network"):
# USB/Network: single set of controls shared with the single channel
quality_options = list(QUALITY_MAP.keys())
default_quality = "Medium (24kHz)" if "Medium (24kHz)" in quality_options else quality_options[0]
@@ -664,15 +1127,13 @@ else:
help="Three-letter language code (e.g., 'eng' for English, 'deu' for German). Used by receivers to display the language of the stream. See: https://en.wikipedia.org/wiki/List_of_ISO_639-3_codes"
)
if audio_mode in ("USB", "Network", "Network - Dante"):
if audio_mode in ("USB", "Network"):
if not is_streaming:
try:
if audio_mode == "USB":
endpoint = "/audio_inputs_pw_usb"
elif audio_mode == "Network":
endpoint = "/audio_inputs_pw_network"
else: # Network - Dante
endpoint = "/audio_inputs_dante"
resp = requests.get(f"{BACKEND_URL}{endpoint}")
device_list = resp.json().get('inputs', [])
@@ -688,6 +1149,7 @@ else:
device_names = [d['name'] for d in device_list]
default_input_name = saved_settings.get('input_device')
# If saved device isn't in current mode's device list, use first available
if default_input_name not in device_names and device_names:
default_input_name = device_names[0]
default_input_label = None
@@ -700,11 +1162,9 @@ else:
"No USB audio input devices found. Connect a USB input and click Refresh."
if audio_mode == "USB" else
"No AES67/Network inputs found."
if audio_mode == "Network" else
"No Dante inputs found. Check asound.conf configuration."
)
st.warning(warn_text)
refresh_key = "refresh_usb" if audio_mode == "USB" else "refresh_aes67" if audio_mode == "Network" else "refresh_dante"
refresh_key = "refresh_usb" if audio_mode == "USB" else "refresh_aes67"
if st.button("Refresh", key=refresh_key, disabled=is_streaming):
try:
r = requests.post(f"{BACKEND_URL}/refresh_audio_devices", timeout=8)
@@ -723,7 +1183,7 @@ else:
index=input_options.index(default_input_label) if default_input_label in input_options else 0
)
with col2:
refresh_key = "refresh_inputs" if audio_mode == "USB" else "refresh_inputs_net" if audio_mode == "Network" else "refresh_inputs_dante"
refresh_key = "refresh_inputs" if audio_mode == "USB" else "refresh_inputs_net"
if st.button("Refresh", key=refresh_key, disabled=is_streaming):
try:
r = requests.post(f"{BACKEND_URL}/refresh_audio_devices", timeout=8)
@@ -746,111 +1206,173 @@ else:
else:
input_device = None
start_stream, stop_stream = render_stream_controls(is_streaming, "Start Auracast", "Stop Auracast", running_mode)
if stop_stream:
st.session_state['stream_started'] = False
try:
r = requests.post(f"{BACKEND_URL}/stop_audio").json()
if r['was_running']:
is_stopped = True
except Exception as e:
st.error(f"Error: {e}")
if stop_stream:
st.session_state['stream_started'] = False
try:
r = requests.post(f"{BACKEND_URL}/stop_audio").json()
if r['was_running']:
is_stopped = True
except Exception as e:
st.error(f"Error: {e}")
if start_stream:
# Always send stop to ensure backend is in a clean state, regardless of current status
r = requests.post(f"{BACKEND_URL}/stop_audio").json()
# Small pause lets backend fully release audio devices before re-init
time.sleep(1)
if start_stream:
# Always send stop to ensure backend is in a clean state, regardless of current status
r = requests.post(f"{BACKEND_URL}/stop_audio").json()
# Small pause lets backend fully release audio devices before re-init
time.sleep(1)
if audio_mode == "Analog":
# Build separate configs per radio, each with its own quality and QoS parameters.
is_started = False
if audio_mode == "Analog":
# Build separate configs per radio, each with its own quality and QoS parameters.
is_started = False
def _build_group_from_radio(cfg: dict) -> auracast_config.AuracastConfigGroup | None:
if not cfg or not cfg.get('input_device'):
return None
q = QUALITY_MAP[cfg['quality']]
return auracast_config.AuracastConfigGroup(
auracast_sampling_rate_hz=q['rate'],
octets_per_frame=q['octets'],
transport='', # is set in backend
assisted_listening_stream=bool(cfg['assisted_listening']),
immediate_rendering=bool(cfg['immediate_rendering']),
presentation_delay_us=int(cfg['presentation_delay_ms'] * 1000),
qos_config=QOS_PRESET_MAP[cfg['qos_preset']],
bigs=[
auracast_config.AuracastBigConfig(
id=cfg.get('id', 123456),
code=(cfg['stream_passwort'].strip() or None),
name=cfg['name'],
program_info=cfg['program_info'],
language=cfg['language'],
audio_source=f"device:{cfg['input_device']}",
input_format=f"int16le,{q['rate']},1",
iso_que_len=1,
sampling_frequency=q['rate'],
octets_per_frame=q['octets'],
)
],
)
# Radio 1 (always active if a device is selected)
config1 = _build_group_from_radio(radio1_cfg)
# Radio 2 (optional)
config2 = _build_group_from_radio(radio2_cfg) if radio2_enabled else None
try:
if config1 is not None:
r1 = requests.post(f"{BACKEND_URL}/init", json=config1.model_dump())
if r1.status_code == 200:
is_started = True
else:
st.error(f"Failed to initialize Radio 1: {r1.text}")
else:
st.error("Radio 1 has no valid input device configured.")
if config2 is not None:
r2 = requests.post(f"{BACKEND_URL}/init2", json=config2.model_dump())
if r2.status_code != 200:
st.error(f"Failed to initialize Radio 2: {r2.text}")
except Exception as e:
st.error(f"Error while starting Analog radios: {e}")
else:
# USB/Network: single config as before, using shared controls
q = QUALITY_MAP[quality]
config = auracast_config.AuracastConfigGroup(
def _build_group_from_radio(cfg: dict) -> auracast_config.AuracastConfigGroup | None:
if not cfg or not cfg.get('input_device'):
return None
q = QUALITY_MAP[cfg['quality']]
return auracast_config.AuracastConfigGroup(
auracast_sampling_rate_hz=q['rate'],
octets_per_frame=q['octets'],
transport='', # is set in backend
assisted_listening_stream=assisted_listening,
immediate_rendering=immediate_rendering,
presentation_delay_us=int(presentation_delay_ms * 1000),
qos_config=QOS_PRESET_MAP[qos_preset],
transport='', # is set in backend
assisted_listening_stream=bool(cfg['assisted_listening']),
immediate_rendering=bool(cfg['immediate_rendering']),
presentation_delay_us=int(cfg['presentation_delay_ms'] * 1000),
qos_config=QOS_PRESET_MAP[cfg['qos_preset']],
bigs=[
auracast_config.AuracastBigConfig(
code=(stream_passwort.strip() or None),
name=stream_name,
program_info=program_info,
language=language,
audio_source=(f"device:{input_device}"),
input_format=(f"int16le,{q['rate']},1"),
id=cfg.get('id', 123456),
code=(cfg['stream_passwort'].strip() or None),
name=cfg['name'],
program_info=cfg['program_info'],
language=cfg['language'],
audio_source=f"device:{cfg['input_device']}",
input_format=f"int16le,{q['rate']},1",
iso_que_len=1,
sampling_frequency=q['rate'],
octets_per_frame=q['octets'],
),
)
],
)
try:
r = requests.post(f"{BACKEND_URL}/init", json=config.model_dump())
if r.status_code == 200:
# Radio 1 (always active if a device is selected)
config1 = _build_group_from_radio(radio1_cfg)
# Radio 2 (optional)
config2 = _build_group_from_radio(radio2_cfg) if radio2_enabled else None
try:
if config1 is not None:
r1 = requests.post(f"{BACKEND_URL}/init", json=config1.model_dump())
if r1.status_code == 200:
is_started = True
else:
st.error(f"Failed to initialize: {r.text}")
except Exception as e:
st.error(f"Error: {e}")
st.error(f"Failed to initialize Radio 1: {r1.text}")
else:
st.error("Radio 1 has no valid input device configured.")
if config2 is not None:
r2 = requests.post(f"{BACKEND_URL}/init2", json=config2.model_dump())
if r2.status_code != 200:
st.error(f"Failed to initialize Radio 2: {r2.text}")
except Exception as e:
st.error(f"Error while starting Analog radios: {e}")
elif audio_mode == "Network - Dante":
# Build multi-stream configs for Dante radios
is_started = False
def _build_dante_radio_config(radio_cfg: dict, radio_id: int) -> auracast_config.AuracastConfigGroup | None:
if not radio_cfg or not radio_cfg.get('streams'):
return None
q = QUALITY_MAP[radio_cfg['quality']]
bigs = []
for i, stream in enumerate(radio_cfg['streams']):
if not stream.get('input_device'):
continue
stream_id = radio_id * 1000 + i + 1 # Unique ID per stream
bigs.append(auracast_config.AuracastBigConfig(
id=stream_id,
code=(stream.get('stream_password', '').strip() or None),
name=stream['name'],
program_info=stream['program_info'],
language=stream['language'],
audio_source=f"device:{stream['input_device']}",
input_format=f"int16le,{q['rate']},1",
iso_que_len=1,
sampling_frequency=q['rate'],
octets_per_frame=q['octets'],
))
if not bigs:
return None
return auracast_config.AuracastConfigGroup(
auracast_sampling_rate_hz=q['rate'],
octets_per_frame=q['octets'],
transport='', # is set in backend
assisted_listening_stream=bool(radio_cfg['assisted_listening']),
immediate_rendering=bool(radio_cfg['immediate_rendering']),
presentation_delay_us=int(radio_cfg['presentation_delay_ms'] * 1000),
qos_config=QOS_PRESET_MAP[radio_cfg['qos_preset']],
bigs=bigs
)
# Radio 1 config
config1 = _build_dante_radio_config(radio1_cfg, 1)
# Radio 2 config (optional)
config2 = _build_dante_radio_config(radio2_cfg, 2) if radio2_cfg else None
try:
if config1 is not None:
r1 = requests.post(f"{BACKEND_URL}/init", json=config1.model_dump())
if r1.status_code == 200:
is_started = True
else:
st.error(f"Failed to initialize Dante Radio 1: {r1.text}")
else:
st.error("Dante Radio 1 has no valid input devices configured.")
if config2 is not None:
r2 = requests.post(f"{BACKEND_URL}/init2", json=config2.model_dump())
if r2.status_code != 200:
st.error(f"Failed to initialize Dante Radio 2: {r2.text}")
except Exception as e:
st.error(f"Error while starting Dante radios: {e}")
else:
# USB/Network: single config as before, using shared controls
q = QUALITY_MAP[quality]
config = auracast_config.AuracastConfigGroup(
auracast_sampling_rate_hz=q['rate'],
octets_per_frame=q['octets'],
transport='', # is set in backend
assisted_listening_stream=assisted_listening,
immediate_rendering=immediate_rendering,
presentation_delay_us=int(presentation_delay_ms * 1000),
qos_config=QOS_PRESET_MAP[qos_preset],
bigs=[
auracast_config.AuracastBigConfig(
code=(stream_passwort.strip() or None),
name=stream_name,
program_info=program_info,
language=language,
audio_source=(f"device:{input_device}"),
input_format=(f"int16le,{q['rate']},1"),
iso_que_len=1,
sampling_frequency=q['rate'],
octets_per_frame=q['octets'],
),
],
)
try:
r = requests.post(f"{BACKEND_URL}/init", json=config.model_dump())
if r.status_code == 200:
is_started = True
else:
st.error(f"Failed to initialize: {r.text}")
except Exception as e:
st.error(f"Error: {e}")
# Centralized rerun based on start/stop outcomes
if is_started or is_stopped:
+41 -8
View File
@@ -369,23 +369,56 @@ async def init_radio(transport: str, conf: auracast_config.AuracastConfigGroup,
big.audio_source = f'device:{device_index}'
# Configure input format based on device type
# IMPORTANT: All hardware devices (Analog ch1/ch2, Dante, USB, Network) only support 48kHz
# We always capture at 48kHz and let the LC3 encoder handle downsampling to target rates
if input_device_name in dante_channels:
# For Dante channels, use mono (1 channel) from shared device
max_in = 1
channels = 1
# Always use 48kHz for hardware capture, regardless of target quality
hardware_capture_rate = 48000
for big in conf.bigs:
big.input_format = f"int16le,{hardware_capture_rate},{channels}"
elif input_device_name in ('ch1', 'ch2'):
# For Analog channels, use mono (1 channel)
max_in = 1
channels = 1
# Always use 48kHz for hardware capture, regardless of target quality
hardware_capture_rate = 48000
for big in conf.bigs:
big.input_format = f"int16le,{hardware_capture_rate},{channels}"
else:
# For USB/Network devices, check device capabilities
devinfo = sd.query_devices(device_index)
max_in = int(devinfo.get('max_input_channels') or 1)
channels = max(1, min(2, max_in))
for big in conf.bigs:
big.input_format = f"int16le,{48000},{channels}"
# Always use 48kHz for hardware capture, regardless of target quality
hardware_capture_rate = 48000
for big in conf.bigs:
big.input_format = f"int16le,{hardware_capture_rate},{channels}"
# For Dante channels, force 48000 Hz sampling rate
if input_device_name in dante_channels:
conf.auracast_sampling_rate_hz = 48000
# Also update octets per frame for 48000 Hz
conf.octets_per_frame = 120 # 48000 Hz setting
# The config group keeps the target sampling rate for LC3 encoder
# The audio input will capture at 48kHz and LC3 encoder will downsample
target_sampling_rate = getattr(conf, 'auracast_sampling_rate_hz', None)
if target_sampling_rate is None and conf.bigs:
target_sampling_rate = getattr(conf.bigs[0], 'sampling_frequency', 48000)
if target_sampling_rate is None:
target_sampling_rate = 48000
# Keep the config group sampling rate as set by frontend
conf.auracast_sampling_rate_hz = target_sampling_rate
# Ensure octets_per_frame matches the target sampling rate
if target_sampling_rate == 48000:
conf.octets_per_frame = 120
elif target_sampling_rate == 32000:
conf.octets_per_frame = 80
elif target_sampling_rate == 24000:
conf.octets_per_frame = 60
elif target_sampling_rate == 16000:
conf.octets_per_frame = 40
else:
conf.octets_per_frame = 120 # default to 48000 setting
conf.qos_config.max_transport_latency_ms = int(conf.qos_config.number_of_retransmissions) * 10 + 3