Adds stereo for dante. TODO: Needs improvement, creates 5x5 ALSA devices.

This commit is contained in:
pober
2026-01-19 16:10:23 +01:00
committed by pstruebi
parent c0d8a5f13d
commit 59f9c91b62
3 changed files with 498 additions and 81 deletions
+238 -81
View File
@@ -725,18 +725,74 @@ else:
help="Radio 1 is always enabled, Radio 2 can be turned on or off."
)
# Stream count dropdown for Radio 1
r1_stream_options = list(dante_stream_options.keys())
# Dante stereo mode toggle
saved_r1_config = saved_settings.get('dante_radio1', {})
dante_stereo_enabled = st.checkbox(
"🎧 Stereo Mode",
value=bool(saved_r1_config.get('dante_stereo_mode', False)),
help="Enable stereo streaming for Dante inputs. Select left and right channels from ASRC channels 1-6. Radio 2 and multi-stream configurations will be disabled in stereo mode.",
disabled=is_streaming
)
# Dante stereo channel selectors
dante_left_channel = None
dante_right_channel = None
if dante_stereo_enabled:
dante_channel_options = ["dante_asrc_ch1", "dante_asrc_ch2", "dante_asrc_ch3",
"dante_asrc_ch4", "dante_asrc_ch5", "dante_asrc_ch6"]
dante_channel_labels = ["CH1", "CH2", "CH3", "CH4", "CH5", "CH6"]
col_left, col_right = st.columns(2)
with col_left:
saved_left = saved_r1_config.get('dante_stereo_left', 'dante_asrc_ch1')
left_idx = dante_channel_options.index(saved_left) if saved_left in dante_channel_options else 0
dante_left_channel = st.selectbox(
"Left Channel",
dante_channel_options,
index=left_idx,
format_func=lambda x: f"ASRC {dante_channel_labels[dante_channel_options.index(x)]}",
disabled=is_streaming,
help="Select the Dante ASRC channel for the left stereo channel"
)
with col_right:
saved_right = saved_r1_config.get('dante_stereo_right', 'dante_asrc_ch2')
right_idx = dante_channel_options.index(saved_right) if saved_right in dante_channel_options else 1
dante_right_channel = st.selectbox(
"Right Channel",
dante_channel_options,
index=right_idx,
format_func=lambda x: f"ASRC {dante_channel_labels[dante_channel_options.index(x)]}",
disabled=is_streaming,
help="Select the Dante ASRC channel for the right stereo channel"
)
if dante_left_channel == dante_right_channel:
st.warning("⚠️ Left and right channels are the same. Select different channels for true stereo.")
else:
st.info(f"🎧 Stereo mode: {dante_channel_labels[dante_channel_options.index(dante_left_channel)]} (Left) + {dante_channel_labels[dante_channel_options.index(dante_right_channel)]} (Right)")
# Stream count dropdown for Radio 1 (disabled in stereo mode - forced to 1 stream at 48kHz)
r1_stream_options = list(dante_stream_options.keys())
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"
)
if dante_stereo_enabled:
# Stereo mode: force 1 stream at 48kHz
r1_stream_config = "1 × 48kHz"
st.selectbox(
"Stream Configuration (Radio 1)",
["1 × 48kHz (Stereo)"],
index=0,
disabled=True,
help="In stereo mode, only 1 stream at 48kHz is supported"
)
else:
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"]
@@ -799,30 +855,34 @@ else:
)
# Per-stream configuration for Radio 1
st.write("**Stream Configuration (Radio 1)**")
if dante_stereo_enabled:
st.write("**Stereo Stream Configuration (Radio 1)**")
else:
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):
if dante_stereo_enabled:
# Stereo mode: single stream with combined L+R channels
with st.expander("Stereo Stream - Radio 1", expanded=True):
saved_streams = saved_r1_config.get('streams', [])
saved_stream = saved_streams[i] if i < len(saved_streams) else {}
saved_stream = saved_streams[0] if saved_streams else {}
# First row: Channel name and language
col_name, col_lang = st.columns([2, 1])
# 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_R1_S{i+1}'),
key=f"r1_stream_{i}_name"
"Channel Name",
value=saved_stream.get('name', 'Dante_Stereo'),
key="r1_stereo_name"
)
with col_lang:
with col_pwd:
stream_password = st.text_input(
f"Stream Password",
"Stream Password",
value=saved_stream.get('stream_password', ''),
type="password",
key=f"r1_stream_{i}_password",
key="r1_stereo_password",
help="Optional: Set a broadcast code for this stream"
)
@@ -831,86 +891,161 @@ else:
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"
"Program Info",
value=saved_stream.get('program_info', 'Dante Stereo Broadcast'),
key="r1_stereo_program"
)
with col_lang_code:
language = st.text_input(
f"Language",
"Language",
value=saved_stream.get('language', 'eng'),
key=f"r1_stream_{i}_lang",
key="r1_stereo_lang",
help="ISO 639-3 language code"
)
# Third row: Input device
col_device = st.columns([1])[0]
# Build stereo device name from selected channels
# Extract channel numbers from dante_asrc_chX
left_ch_num = dante_left_channel.replace('dante_asrc_ch', '') if dante_left_channel else '1'
right_ch_num = dante_right_channel.replace('dante_asrc_ch', '') if dante_right_channel else '2'
# Use the device name that matches the user's L/R selection
stereo_device_name = f"dante_stereo_{left_ch_num}_{right_ch_num}"
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
# Show stereo device info
st.selectbox(
"Input Device (Stereo)",
[f"Stereo: CH{left_ch_num} (L) + CH{right_ch_num} (R)"],
index=0,
disabled=True,
help="Stereo input from the selected Dante ASRC channels"
)
r1_streams.append({
'name': stream_name,
'program_info': program_info,
'language': language,
'input_device': input_device,
'stream_password': stream_password
'input_device': stereo_device_name,
'stream_password': stream_password,
'dante_stereo_mode': True,
'dante_stereo_left': dante_left_channel,
'dante_stereo_right': dante_right_channel,
})
else:
# Normal mono mode: multiple streams with individual channels
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 ---
with st.container(border=True):
st.subheader("Radio 2")
# Enable/disable checkbox for Radio 2
# Disable Radio 2 in stereo mode
saved_r2_config = saved_settings.get('dante_radio2', {})
radio2_enabled_default = secondary_is_streaming
radio2_enabled = st.checkbox(
"Enable Radio 2",
value=radio2_enabled_default,
help="Activate a second Dante radio with its own quality and timing settings."
)
if dante_stereo_enabled:
st.info("🎧 Radio 2 is automatically disabled in stereo mode")
radio2_enabled = False
else:
# Enable/disable checkbox for Radio 2
radio2_enabled_default = secondary_is_streaming
radio2_enabled = st.checkbox(
"Enable Radio 2",
value=radio2_enabled_default,
help="Activate a second Dante radio with its own quality and timing settings."
)
if radio2_enabled:
# Stream count dropdown for Radio 2
@@ -1086,6 +1221,14 @@ else:
})
else:
r2_streams = []
# Set default values for r2_* variables when radio2 is disabled
r2_stream_config = '1 × 48kHz'
r2_quality = 'High (48kHz)'
r2_radio_quality = 'High (48kHz)'
r2_assisted_listening = False
r2_immediate_rendering = False
r2_presentation_delay_ms = 40
r2_qos_preset = 'Fast'
# Validate unique input devices for Network - Dante mode
if audio_mode == "Network - Dante":
@@ -1117,6 +1260,9 @@ else:
'immediate_rendering': r1_immediate_rendering,
'presentation_delay_ms': r1_presentation_delay_ms,
'qos_preset': r1_qos_preset,
'dante_stereo_mode': dante_stereo_enabled,
'dante_stereo_left': dante_left_channel,
'dante_stereo_right': dante_right_channel,
}
radio2_cfg = {
@@ -1360,22 +1506,33 @@ if start_stream:
q = QUALITY_MAP[radio_cfg['quality']]
bigs = []
# Check if stereo mode is enabled for this radio
is_stereo_mode = bool(radio_cfg.get('dante_stereo_mode', False))
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
# Check if this specific stream uses stereo (dante_stereo_X_Y device)
input_device = stream['input_device']
stream_is_stereo = is_stereo_mode or input_device.startswith('dante_stereo_')
num_bis = 2 if stream_is_stereo else 1
num_channels = 2 if stream_is_stereo else 1
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",
audio_source=f"device:{input_device}",
input_format=f"int16le,{q['rate']},{num_channels}",
iso_que_len=1,
sampling_frequency=q['rate'],
octets_per_frame=q['octets'],
num_bis=num_bis,
))
if not bigs:
+15
View File
@@ -357,6 +357,21 @@ async def init_radio(transport: str, conf: auracast_config.AuracastConfigGroup,
big.audio_source = f'alsa:{sel}'
big.input_format = f"int16le,{hardware_capture_rate},1"
continue
# Dante stereo devices: dante_stereo_X_Y (e.g., dante_stereo_1_2)
if sel and sel.startswith('dante_stereo_'):
is_stereo = getattr(big, 'num_bis', 1) == 2
if is_stereo:
# Stereo mode: use the stereo ALSA device with 2 channels
big.audio_source = f'alsa:{sel}'
big.input_format = f"int16le,{hardware_capture_rate},2"
log.info("Configured Dante stereo input: using ALSA %s with 2 channels", sel)
else:
# Fallback to mono if num_bis != 2 (shouldn't happen)
big.audio_source = f'alsa:{sel}'
big.input_format = f"int16le,{hardware_capture_rate},2"
log.warning("Dante stereo device %s used but num_bis=%d, capturing as stereo anyway", sel, getattr(big, 'num_bis', 1))
continue
if sel in ('ch1', 'ch2'):
# Analog channels: check if this should be stereo based on num_bis
+245
View File
@@ -99,3 +99,248 @@ pcm.dante_asrc_ch6 {
ttable.0.5 1
hint { show on ; description "DEP RX CH6" }
}
# ---- Stereo devices for Dante (combine any two channels as L+R) ----
# These devices route selected source channels to stereo output
# Format: dante_stereo_<left_ch>_<right_ch>
pcm.dante_stereo_1_2 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.0 1 # Left channel from ch1
ttable.1.1 1 # Right channel from ch2
hint { show on ; description "DEP RX Stereo CH1+CH2" }
}
pcm.dante_stereo_1_3 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.0 1
ttable.1.2 1
hint { show on ; description "DEP RX Stereo CH1+CH3" }
}
pcm.dante_stereo_1_4 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.0 1
ttable.1.3 1
hint { show on ; description "DEP RX Stereo CH1+CH4" }
}
pcm.dante_stereo_1_5 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.0 1
ttable.1.4 1
hint { show on ; description "DEP RX Stereo CH1+CH5" }
}
pcm.dante_stereo_1_6 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.0 1
ttable.1.5 1
hint { show on ; description "DEP RX Stereo CH1+CH6" }
}
pcm.dante_stereo_2_3 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.1 1
ttable.1.2 1
hint { show on ; description "DEP RX Stereo CH2+CH3" }
}
pcm.dante_stereo_2_4 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.1 1
ttable.1.3 1
hint { show on ; description "DEP RX Stereo CH2+CH4" }
}
pcm.dante_stereo_2_5 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.1 1
ttable.1.4 1
hint { show on ; description "DEP RX Stereo CH2+CH5" }
}
pcm.dante_stereo_2_6 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.1 1
ttable.1.5 1
hint { show on ; description "DEP RX Stereo CH2+CH6" }
}
pcm.dante_stereo_3_4 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.2 1
ttable.1.3 1
hint { show on ; description "DEP RX Stereo CH3+CH4" }
}
pcm.dante_stereo_3_5 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.2 1
ttable.1.4 1
hint { show on ; description "DEP RX Stereo CH3+CH5" }
}
pcm.dante_stereo_3_6 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.2 1
ttable.1.5 1
hint { show on ; description "DEP RX Stereo CH3+CH6" }
}
pcm.dante_stereo_4_5 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.3 1
ttable.1.4 1
hint { show on ; description "DEP RX Stereo CH4+CH5" }
}
pcm.dante_stereo_4_6 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.3 1
ttable.1.5 1
hint { show on ; description "DEP RX Stereo CH4+CH6" }
}
pcm.dante_stereo_5_6 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.4 1
ttable.1.5 1
hint { show on ; description "DEP RX Stereo CH5+CH6" }
}
# ---- Reverse stereo devices (for when left channel > right channel) ----
pcm.dante_stereo_2_1 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.1 1 # Left from ch2
ttable.1.0 1 # Right from ch1
hint { show on ; description "DEP RX Stereo CH2+CH1" }
}
pcm.dante_stereo_3_1 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.2 1
ttable.1.0 1
hint { show on ; description "DEP RX Stereo CH3+CH1" }
}
pcm.dante_stereo_3_2 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.2 1
ttable.1.1 1
hint { show on ; description "DEP RX Stereo CH3+CH2" }
}
pcm.dante_stereo_4_1 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.3 1
ttable.1.0 1
hint { show on ; description "DEP RX Stereo CH4+CH1" }
}
pcm.dante_stereo_4_2 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.3 1
ttable.1.1 1
hint { show on ; description "DEP RX Stereo CH4+CH2" }
}
pcm.dante_stereo_4_3 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.3 1
ttable.1.2 1
hint { show on ; description "DEP RX Stereo CH4+CH3" }
}
pcm.dante_stereo_5_1 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.4 1
ttable.1.0 1
hint { show on ; description "DEP RX Stereo CH5+CH1" }
}
pcm.dante_stereo_5_2 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.4 1
ttable.1.1 1
hint { show on ; description "DEP RX Stereo CH5+CH2" }
}
pcm.dante_stereo_5_3 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.4 1
ttable.1.2 1
hint { show on ; description "DEP RX Stereo CH5+CH3" }
}
pcm.dante_stereo_5_4 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.4 1
ttable.1.3 1
hint { show on ; description "DEP RX Stereo CH5+CH4" }
}
pcm.dante_stereo_6_1 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.5 1
ttable.1.0 1
hint { show on ; description "DEP RX Stereo CH6+CH1" }
}
pcm.dante_stereo_6_2 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.5 1
ttable.1.1 1
hint { show on ; description "DEP RX Stereo CH6+CH2" }
}
pcm.dante_stereo_6_3 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.5 1
ttable.1.2 1
hint { show on ; description "DEP RX Stereo CH6+CH3" }
}
pcm.dante_stereo_6_4 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.5 1
ttable.1.3 1
hint { show on ; description "DEP RX Stereo CH6+CH4" }
}
pcm.dante_stereo_6_5 {
type route
slave { pcm "dante_asrc_shared6"; channels 6; }
ttable.0.5 1
ttable.1.4 1
hint { show on ; description "DEP RX Stereo CH6+CH5" }
}