2 channels per radio are working, 3 are shaky. UI is now robust.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user