fix(audio): simplify uac sink and prebuild sync harness
This commit is contained in:
parent
caca212e71
commit
74706aaf32
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1642,7 +1642,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.13.12"
|
version = "0.13.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1676,7 +1676,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.13.12"
|
version = "0.13.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1688,7 +1688,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.13.12"
|
version = "0.13.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.13.12"
|
version = "0.13.13"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.13.12"
|
version = "0.13.13"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -278,7 +278,7 @@
|
|||||||
},
|
},
|
||||||
"server/src/audio/voice_input.rs": {
|
"server/src/audio/voice_input.rs": {
|
||||||
"line_percent": 100.0,
|
"line_percent": 100.0,
|
||||||
"loc": 426
|
"loc": 469
|
||||||
},
|
},
|
||||||
"server/src/bin/lesavka_uvc/control_payloads.rs": {
|
"server/src/bin/lesavka_uvc/control_payloads.rs": {
|
||||||
"line_percent": 100.0,
|
"line_percent": 100.0,
|
||||||
|
|||||||
@ -22,8 +22,12 @@ VIDEO_SIZE=${VIDEO_SIZE:-1280x720}
|
|||||||
VIDEO_FPS=${VIDEO_FPS:-30}
|
VIDEO_FPS=${VIDEO_FPS:-30}
|
||||||
VIDEO_FORMAT=${VIDEO_FORMAT:-mjpeg}
|
VIDEO_FORMAT=${VIDEO_FORMAT:-mjpeg}
|
||||||
REMOTE_AUDIO_SOURCE=${REMOTE_AUDIO_SOURCE:-auto}
|
REMOTE_AUDIO_SOURCE=${REMOTE_AUDIO_SOURCE:-auto}
|
||||||
|
REMOTE_AUDIO_QUIESCE_USER_AUDIO=${REMOTE_AUDIO_QUIESCE_USER_AUDIO:-auto}
|
||||||
SSH_OPTS=${SSH_OPTS:-"-o BatchMode=yes -o ConnectTimeout=5"}
|
SSH_OPTS=${SSH_OPTS:-"-o BatchMode=yes -o ConnectTimeout=5"}
|
||||||
LOCAL_AUDIO_SANITY=${LOCAL_AUDIO_SANITY:-1}
|
LOCAL_AUDIO_SANITY=${LOCAL_AUDIO_SANITY:-1}
|
||||||
|
PROBE_PREBUILD=${PROBE_PREBUILD:-1}
|
||||||
|
PROBE_BIN=${PROBE_BIN:-"${REPO_ROOT}/target/debug/lesavka-sync-probe"}
|
||||||
|
ANALYZE_BIN=${ANALYZE_BIN:-"${REPO_ROOT}/target/debug/lesavka-sync-analyze"}
|
||||||
|
|
||||||
mkdir -p "${LOCAL_OUTPUT_DIR}"
|
mkdir -p "${LOCAL_OUTPUT_DIR}"
|
||||||
STAMP="$(date +%Y%m%d-%H%M%S)"
|
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
@ -34,6 +38,23 @@ if [[ "${LOCAL_AUDIO_SANITY}" != "0" ]]; then
|
|||||||
"${SCRIPT_DIR}/run_local_audio_sanity.sh"
|
"${SCRIPT_DIR}/run_local_audio_sanity.sh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${PROBE_PREBUILD}" != "0" ]]; then
|
||||||
|
echo "==> prebuilding sync probe/analyzer before opening the capture window"
|
||||||
|
(
|
||||||
|
cd "${REPO_ROOT}"
|
||||||
|
cargo build -p lesavka_client --bin lesavka-sync-probe --bin lesavka-sync-analyze
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "${PROBE_BIN}" ]]; then
|
||||||
|
echo "sync probe binary not found at ${PROBE_BIN}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ ! -x "${ANALYZE_BIN}" ]]; then
|
||||||
|
echo "sync analyzer binary not found at ${ANALYZE_BIN}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
echo "==> starting Tethys capture on ${TETHYS_HOST}"
|
echo "==> starting Tethys capture on ${TETHYS_HOST}"
|
||||||
ssh ${SSH_OPTS} "${TETHYS_HOST}" bash -s -- \
|
ssh ${SSH_OPTS} "${TETHYS_HOST}" bash -s -- \
|
||||||
"${REMOTE_CAPTURE}" \
|
"${REMOTE_CAPTURE}" \
|
||||||
@ -41,7 +62,8 @@ ssh ${SSH_OPTS} "${TETHYS_HOST}" bash -s -- \
|
|||||||
"${VIDEO_SIZE}" \
|
"${VIDEO_SIZE}" \
|
||||||
"${VIDEO_FPS}" \
|
"${VIDEO_FPS}" \
|
||||||
"${VIDEO_FORMAT}" \
|
"${VIDEO_FORMAT}" \
|
||||||
"${REMOTE_AUDIO_SOURCE}" <<'REMOTE_CAPTURE_SCRIPT' &
|
"${REMOTE_AUDIO_SOURCE}" \
|
||||||
|
"${REMOTE_AUDIO_QUIESCE_USER_AUDIO}" <<'REMOTE_CAPTURE_SCRIPT' &
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
remote_capture=$1
|
remote_capture=$1
|
||||||
capture_seconds=$2
|
capture_seconds=$2
|
||||||
@ -49,6 +71,7 @@ video_size=$3
|
|||||||
video_fps=$4
|
video_fps=$4
|
||||||
video_format=$5
|
video_format=$5
|
||||||
remote_audio_source=$6
|
remote_audio_source=$6
|
||||||
|
remote_audio_quiesce_user_audio=$7
|
||||||
|
|
||||||
rm -f "${remote_capture}"
|
rm -f "${remote_capture}"
|
||||||
video_args=(-f video4linux2 -framerate "${video_fps}" -video_size "${video_size}")
|
video_args=(-f video4linux2 -framerate "${video_fps}" -video_size "${video_size}")
|
||||||
@ -56,6 +79,18 @@ if [[ -n "${video_format}" ]]; then
|
|||||||
video_args+=(-input_format "${video_format}")
|
video_args+=(-input_format "${video_format}")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
restore_user_audio() {
|
||||||
|
systemctl --user start pipewire.socket pipewire-pulse.socket wireplumber.service >/dev/null 2>&1 || true
|
||||||
|
sleep 1
|
||||||
|
systemctl --user start pipewire.service pipewire-pulse.service >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
quiesce_user_audio() {
|
||||||
|
systemctl --user stop pipewire-pulse.service pipewire.service wireplumber.service \
|
||||||
|
pipewire-pulse.socket pipewire.socket >/dev/null 2>&1 || true
|
||||||
|
sleep 1
|
||||||
|
}
|
||||||
|
|
||||||
resolve_pulse_source() {
|
resolve_pulse_source() {
|
||||||
if ! command -v pactl >/dev/null 2>&1; then
|
if ! command -v pactl >/dev/null 2>&1; then
|
||||||
return 1
|
return 1
|
||||||
@ -91,6 +126,31 @@ else
|
|||||||
exit 64
|
exit 64
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
quiesce_for_alsa=0
|
||||||
|
case "${remote_audio_quiesce_user_audio}" in
|
||||||
|
1|true|yes)
|
||||||
|
quiesce_for_alsa=1
|
||||||
|
;;
|
||||||
|
auto)
|
||||||
|
if [[ "${audio_mode}" == "alsa" ]]; then
|
||||||
|
quiesce_for_alsa=1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
0|false|no)
|
||||||
|
quiesce_for_alsa=0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'unsupported REMOTE_AUDIO_QUIESCE_USER_AUDIO=%s\n' "${remote_audio_quiesce_user_audio}" >&2
|
||||||
|
exit 64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ "${quiesce_for_alsa}" == "1" ]]; then
|
||||||
|
printf 'quiescing Tethys user audio before raw ALSA capture\n' >&2
|
||||||
|
quiesce_user_audio
|
||||||
|
trap restore_user_audio EXIT
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "${audio_mode}" == "pulse" ]]; then
|
if [[ "${audio_mode}" == "pulse" ]]; then
|
||||||
printf 'using Pulse source: %s\n' "${pulse_source}" >&2
|
printf 'using Pulse source: %s\n' "${pulse_source}" >&2
|
||||||
ffmpeg -hide_banner -loglevel error -y \
|
ffmpeg -hide_banner -loglevel error -y \
|
||||||
@ -126,7 +186,7 @@ echo "==> running local Lesavka sync probe against ${LESAVKA_SERVER_ADDR}"
|
|||||||
probe_status=0
|
probe_status=0
|
||||||
(
|
(
|
||||||
cd "${REPO_ROOT}"
|
cd "${REPO_ROOT}"
|
||||||
cargo run -p lesavka_client --bin lesavka-sync-probe -- \
|
"${PROBE_BIN}" \
|
||||||
--server "${LESAVKA_SERVER_ADDR}" \
|
--server "${LESAVKA_SERVER_ADDR}" \
|
||||||
--duration-seconds "${PROBE_DURATION_SECONDS}" \
|
--duration-seconds "${PROBE_DURATION_SECONDS}" \
|
||||||
--warmup-seconds "${PROBE_WARMUP_SECONDS}"
|
--warmup-seconds "${PROBE_WARMUP_SECONDS}"
|
||||||
@ -158,7 +218,7 @@ fi
|
|||||||
echo "==> analyzing capture"
|
echo "==> analyzing capture"
|
||||||
(
|
(
|
||||||
cd "${REPO_ROOT}"
|
cd "${REPO_ROOT}"
|
||||||
cargo run -p lesavka_client --bin lesavka-sync-analyze -- "${LOCAL_CAPTURE}"
|
"${ANALYZE_BIN}" "${LOCAL_CAPTURE}"
|
||||||
)
|
)
|
||||||
|
|
||||||
echo "==> done"
|
echo "==> done"
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.13.12"
|
version = "0.13.13"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -119,6 +119,10 @@ fn voice_sink_session_clock_align_enabled() -> bool {
|
|||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn voice_sink_delay_queue_enabled(compensation_us: i64) -> bool {
|
||||||
|
compensation_us > 0
|
||||||
|
}
|
||||||
|
|
||||||
impl Voice {
|
impl Voice {
|
||||||
#[cfg(coverage)]
|
#[cfg(coverage)]
|
||||||
pub async fn new(_alsa_dev: &str) -> anyhow::Result<Self> {
|
pub async fn new(_alsa_dev: &str) -> anyhow::Result<Self> {
|
||||||
@ -210,24 +214,31 @@ impl Voice {
|
|||||||
let latency_time_us = voice_sink_latency_time_us();
|
let latency_time_us = voice_sink_latency_time_us();
|
||||||
let compensation_us = voice_sink_compensation_us();
|
let compensation_us = voice_sink_compensation_us();
|
||||||
let clock_align_enabled = voice_sink_session_clock_align_enabled();
|
let clock_align_enabled = voice_sink_session_clock_align_enabled();
|
||||||
|
let compensation_ns = (compensation_us.max(0) as u64).saturating_mul(1_000);
|
||||||
|
let delay_queue_enabled = voice_sink_delay_queue_enabled(compensation_us);
|
||||||
|
|
||||||
alsa_sink.set_property("device", alsa_dev);
|
alsa_sink.set_property("device", alsa_dev);
|
||||||
alsa_sink.set_property("sync", clock_align_enabled);
|
alsa_sink.set_property("sync", clock_align_enabled);
|
||||||
alsa_sink.set_property("async", clock_align_enabled);
|
// The UAC gadget is a live sink, so async preroll buys us little and
|
||||||
|
// has repeatedly stranded post-queue buffers behind a sink that never
|
||||||
|
// fully arms. Keep timestamp sync, but let the sink start immediately.
|
||||||
|
alsa_sink.set_property("async", false);
|
||||||
alsa_sink.set_property("enable-last-sample", false);
|
alsa_sink.set_property("enable-last-sample", false);
|
||||||
alsa_sink.set_property("provide-clock", false);
|
alsa_sink.set_property("provide-clock", false);
|
||||||
alsa_sink.set_property("buffer-time", buffer_time_us);
|
alsa_sink.set_property("buffer-time", buffer_time_us);
|
||||||
alsa_sink.set_property("latency-time", latency_time_us);
|
alsa_sink.set_property("latency-time", latency_time_us);
|
||||||
let compensation_ns = (compensation_us.max(0) as u64).saturating_mul(1_000);
|
if delay_queue_enabled {
|
||||||
delay_queue.set_property("max-size-buffers", 0u32);
|
delay_queue.set_property("max-size-buffers", 0u32);
|
||||||
delay_queue.set_property("max-size-bytes", 0u32);
|
delay_queue.set_property("max-size-bytes", 0u32);
|
||||||
delay_queue.set_property("max-size-time", compensation_ns);
|
delay_queue.set_property("max-size-time", compensation_ns);
|
||||||
delay_queue.set_property("min-threshold-time", compensation_ns);
|
delay_queue.set_property("min-threshold-time", compensation_ns);
|
||||||
|
}
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
%alsa_dev,
|
%alsa_dev,
|
||||||
buffer_time_us,
|
buffer_time_us,
|
||||||
latency_time_us,
|
latency_time_us,
|
||||||
compensation_us,
|
compensation_us,
|
||||||
|
delay_queue_enabled,
|
||||||
clock_align_enabled,
|
clock_align_enabled,
|
||||||
"🎤 UAC sink low-latency timing armed"
|
"🎤 UAC sink low-latency timing armed"
|
||||||
);
|
);
|
||||||
@ -235,27 +246,51 @@ impl Voice {
|
|||||||
crate::media_timing::prepare_pipeline_clock_sync(&pipeline);
|
crate::media_timing::prepare_pipeline_clock_sync(&pipeline);
|
||||||
}
|
}
|
||||||
|
|
||||||
pipeline.add_many([
|
if delay_queue_enabled {
|
||||||
appsrc.upcast_ref(),
|
pipeline.add_many([
|
||||||
&decodebin,
|
appsrc.upcast_ref(),
|
||||||
&convert,
|
&decodebin,
|
||||||
&resample,
|
&convert,
|
||||||
&capsfilter,
|
&resample,
|
||||||
&level,
|
&capsfilter,
|
||||||
&delay_queue,
|
&level,
|
||||||
&post_level,
|
&delay_queue,
|
||||||
&alsa_sink,
|
&post_level,
|
||||||
])?;
|
&alsa_sink,
|
||||||
|
])?;
|
||||||
|
} else {
|
||||||
|
pipeline.add_many([
|
||||||
|
appsrc.upcast_ref(),
|
||||||
|
&decodebin,
|
||||||
|
&convert,
|
||||||
|
&resample,
|
||||||
|
&capsfilter,
|
||||||
|
&level,
|
||||||
|
&post_level,
|
||||||
|
&alsa_sink,
|
||||||
|
])?;
|
||||||
|
}
|
||||||
appsrc.link(&decodebin)?;
|
appsrc.link(&decodebin)?;
|
||||||
gst::Element::link_many([
|
if delay_queue_enabled {
|
||||||
&convert,
|
gst::Element::link_many([
|
||||||
&resample,
|
&convert,
|
||||||
&capsfilter,
|
&resample,
|
||||||
&level,
|
&capsfilter,
|
||||||
&delay_queue,
|
&level,
|
||||||
&post_level,
|
&delay_queue,
|
||||||
&alsa_sink,
|
&post_level,
|
||||||
])?;
|
&alsa_sink,
|
||||||
|
])?;
|
||||||
|
} else {
|
||||||
|
gst::Element::link_many([
|
||||||
|
&convert,
|
||||||
|
&resample,
|
||||||
|
&capsfilter,
|
||||||
|
&level,
|
||||||
|
&post_level,
|
||||||
|
&alsa_sink,
|
||||||
|
])?;
|
||||||
|
}
|
||||||
|
|
||||||
/*------------ decodebin autolink ----------------*/
|
/*------------ decodebin autolink ----------------*/
|
||||||
let convert_sink = convert
|
let convert_sink = convert
|
||||||
@ -423,4 +458,12 @@ mod voice_sink_timing_tests {
|
|||||||
assert!(super::voice_sink_session_clock_align_enabled());
|
assert!(super::voice_sink_session_clock_align_enabled());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delay_queue_turns_on_only_for_positive_compensation() {
|
||||||
|
assert!(!super::voice_sink_delay_queue_enabled(-1));
|
||||||
|
assert!(!super::voice_sink_delay_queue_enabled(0));
|
||||||
|
assert!(super::voice_sink_delay_queue_enabled(1));
|
||||||
|
assert!(super::voice_sink_delay_queue_enabled(90_000));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user