diff --git a/README.md b/README.md index c4fe4b47..d167ea47 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ Some tests support these environment variables (work in progress): - SOF_ALSA_OPTS contains optional parameters passed on both play and record. - SOF_APLAY_OPTS and SOF_ARECORD_OPTS contain optional parameters passed additionally on play and record respectively. These options are applied to the selected tool (alsa or tinyalsa) based on the value of SOF_ALSA_TOOL + - SOF_TEST_PIPEWIRE (default: false) allows to use pipewire instead of ALSA direct mode. Supported by a limited number of tests, + try 'git grep SOF_TEST_PIPEWIRE' to find them. Warning, these environment variables do NOT support parameters with whitespace or globbing characters, in other words this does NOT diff --git a/case-lib/hijack.sh b/case-lib/hijack.sh index c0b94065..8de65fce 100644 --- a/case-lib/hijack.sh +++ b/case-lib/hijack.sh @@ -12,6 +12,8 @@ function func_exit_handler() dlogi "Starting func_exit_handler($exit_status)" + func_lib_check_and_disable_pipewire + # call trace if [ "$exit_status" -ne 0 ] ; then dloge "Starting ${FUNCNAME[0]}(), exit status=$exit_status, FUNCNAME stack:" diff --git a/case-lib/lib.sh b/case-lib/lib.sh index d4f95883..f7cb4b35 100644 --- a/case-lib/lib.sh +++ b/case-lib/lib.sh @@ -83,6 +83,10 @@ minvalue() { printf '%d' $(( "$1" < "$2" ? "$1" : "$2" )); } # start_test() { + if [ "$SOF_TEST_PIPEWIRE" == true ]; then + func_lib_enable_pipewire + fi + if is_subtest; then return 0 fi @@ -571,6 +575,45 @@ func_lib_check_sudo() } } +func_lib_enable_pipewire() +{ + dlogi "Starting Pipewire..." + + sudo systemctl --global unmask pipewire{,-pulse}.{socket,service} + systemctl --user unmask pipewire{,-pulse}.{socket,service} + sudo systemctl --global unmask wireplumber.service + systemctl --user unmask wireplumber.service + + systemctl --user start pipewire{,-pulse}.{socket,service} + systemctl --user start wireplumber.service + + systemctl --user daemon-reload + + systemctl is-active --user --quiet pipewire{,-pulse}.{socket,service} && dlogi "Pipewire started" + systemctl is-active --user --quiet wireplumber.service && dlogi "Wireplumber started" +} + +func_lib_check_and_disable_pipewire() +{ + dlogi "Check if pipewire is running" + + if systemctl is-active --user --quiet pipewire{,-pulse}.{socket,service}; then func_lib_disable_pipewire; else dlogi "Pipewire not running"; fi +} + +func_lib_disable_pipewire() +{ + dlogi "Stopping Pipewire..." + + systemctl --user stop wireplumber.service + systemctl --user stop pipewire{,-pulse}.{socket,service} + + sudo systemctl --global mask wireplumber.service + sudo systemctl --global mask pipewire{,-pulse}.{socket,service} + + if systemctl is-active --user --quiet wireplumber.service; then dlogi "Wireplumber not stopped"; else dlogi "Wireplumber stopped"; fi + if systemctl is-active --user --quiet pipewire{,-pulse}.{socket,service}; then dlogi "Pipewire not stopped"; else dlogi "Pipewire stopped"; fi +} + systemctl_show_pulseaudio() { printf '\n' @@ -908,9 +951,15 @@ aplay_opts() # shellcheck disable=SC2086 tinyplay $SOF_ALSA_OPTS $SOF_APLAY_OPTS -D "$card_nr" -d "$dev_nr" -i wav noise.wav elif [[ "$SOF_ALSA_TOOL" = "alsa" ]]; then - dlogc "aplay $SOF_ALSA_OPTS $SOF_APLAY_OPTS $*" - # shellcheck disable=SC2086 - aplay $SOF_ALSA_OPTS $SOF_APLAY_OPTS "$@" + if [[ "$SOF_TEST_PIPEWIRE" == true ]]; then + dlogc "timeout -k $duration $duration aplay $SOF_ALSA_OPTS $SOF_APLAY_OPTS $*" # option -d doesn't work with pipewire so we need timeout + # shellcheck disable=SC2086 + timeout -k "$duration" "$duration" aplay $SOF_ALSA_OPTS $SOF_APLAY_OPTS "$@" + else + dlogc "aplay $SOF_ALSA_OPTS $SOF_APLAY_OPTS $*" + # shellcheck disable=SC2086 + aplay $SOF_ALSA_OPTS $SOF_APLAY_OPTS "$@" + fi else die "Unknown ALSA tool: ${SOF_ALSA_TOOL}" fi @@ -927,14 +976,38 @@ arecord_opts() # shellcheck disable=SC2086 tinycap $SOF_ALSA_OPTS $SOF_ARECORD_OPTS "$file" -D "$card_nr" -d "$dev_nr" -c "$channel" -t "$duration" -r "$rate" -b "$format" elif [[ "$SOF_ALSA_TOOL" = "alsa" ]]; then - dlogc "arecord $SOF_ALSA_OPTS $SOF_ARECORD_OPTS $*" - # shellcheck disable=SC2086 - arecord $SOF_ALSA_OPTS $SOF_ARECORD_OPTS "$@" + if [[ "$SOF_TEST_PIPEWIRE" == true ]]; then + dlogc "timeout -k $duration $duration arecord $SOF_ALSA_OPTS $SOF_ARECORD_OPTS $*" # option -d doesn't work with pipewire so we need timeout + # shellcheck disable=SC2086 + timeout -k "$duration" "$duration" arecord $SOF_ALSA_OPTS $SOF_ARECORD_OPTS "$@" + else + dlogc "arecord $SOF_ALSA_OPTS $SOF_ARECORD_OPTS $*" + # shellcheck disable=SC2086 + arecord $SOF_ALSA_OPTS $SOF_ARECORD_OPTS "$@" + fi else die "Unknown ALSA tool: ${SOF_ALSA_TOOL}" fi } +# Get the ID of the first sink/source of a given type, e.g. "Speaker" or "Headphones". Print an empty line if ID not found. +get_id_of_pipewire_endpoint() +{ + # $ wpctl status returns list of all endpoints managed by wireplumber. We filter by given sink/source type, which returns something like this: + # │ * 48. sof-soundwire Headphones [vol: 0.40] (or without the * when it's not the current default) + # We filter out everything but ID, and only take the first line of the output (if there's more that one object of that type we ignore the rest) + + local object_name="$1" + object_id=$(wpctl status | awk -v name="$object_name" 'tolower($0) ~ tolower(name) { sub(/\*/,""); sub(/\./,"",$2); print $2; exit }') + + # Check if object_id is a number + re='^[0-9]+$' + if [[ "$object_id" =~ $re ]] ; then + printf '%s' "$object_id" + fi + +} + die() { dloge "$@" diff --git a/test-case/check-performance.sh b/test-case/check-performance.sh index 2b0eecfc..157eed19 100755 --- a/test-case/check-performance.sh +++ b/test-case/check-performance.sh @@ -20,6 +20,9 @@ set -e +PIPEWIRE_OUTPUTS=("Speaker" "Headphones" "HDMI" "Stereo") +PIPEWIRE_INPUTS=("Digital Microphone" "DMIC" "Headset Microphone" "SoundWire microphone" "Stereo") + # It is pointless to perf component in HDMI pipeline, so filter out HDMI pipelines # shellcheck disable=SC2034 NO_HDMI_MODE=true @@ -41,52 +44,112 @@ func_opt_parse_option "$@" tplg=${OPT_VAL['t']} duration=${OPT_VAL['d']} -start_test -logger_disabled || func_lib_start_log_collect - -setup_kernel_check_point -func_lib_check_sudo -func_pipeline_export "$tplg" "type:any" - -aplay_num=0 -arecord_num=0 - -for idx in $(seq 0 $((PIPELINE_COUNT - 1))) -do - channel=$(func_pipeline_parse_value "$idx" channel) - rate=$(func_pipeline_parse_value "$idx" rate) - dev=$(func_pipeline_parse_value "$idx" dev) - pcm=$(func_pipeline_parse_value "$idx" pcm) - type=$(func_pipeline_parse_value "$idx" type) - - # Currently, copier will convert bit depth to S32_LE despite what bit depth - # is used in aplay, so make S32_LE as base bit depth for performance analysis. - fmt=S32_LE - - dlogi "Running (PCM: $pcm [$dev]<$type>) in background" - if [ "$type" == "playback" ]; then - aplay_opts -D "$dev" -c "$channel" -r "$rate" -f "$fmt" -d "$duration" /dev/zero -q & - aplay_num=$((aplay_num+1)) - else - arecord_opts -D "$dev" -c "$channel" -r "$rate" -f "$fmt" -d "$duration" /dev/null -q & - arecord_num=$((arecord_num+1)) - fi -done - -sleep 1 # waiting stable streaming of aplay/arecord -dlogi "Number of aplay/arecord process started: $aplay_num, $arecord_num" -real_aplay_num=$(ps --no-headers -C aplay | wc -l) -real_arecord_num=$(ps --no-headers -C arecord | wc -l) -if [ "$real_aplay_num" != "$aplay_num" ] || [ "$real_arecord_num" != "$arecord_num" ]; -then - dlogi "Number of aplay/arecord process running: $real_aplay_num, $real_arecord_num" - die "aplay/arecord process exit unexpectedly" -fi +# run aplay for all sink types given as a parameter and save the number of started aplay processes +run_aplays() +{ + local sinks=("$@") -dlogi "Waiting for aplay/arecord process to exit" -sleep $((duration + 2)) + for sink_type in "${sinks[@]}" + do + sink_id=$(get_id_of_pipewire_endpoint "$sink_type") + if [ -z "$sink_id" ]; then + dlogi "No $sink_type found, skipping to the next one" + continue + fi + dlogi "Setting default sink to $sink_id: $sink_type" + wpctl set-default "$sink_id" + aplay_opts -D pipewire /dev/zero -q & + aplay_num=$((aplay_num+1)) + done +} + +# run arecord for all source types given as a parameter and save the number of started arecord processes +run_arecords() +{ + local sources=("$@") + + for source_type in "${sources[@]}" + do + source_id=$(get_id_of_pipewire_endpoint "$source_type") + if [ -z "$source_id" ]; then + dlogi "No $source_type found, skipping to the next one" + continue + fi + dlogi "Setting default source to $source_id: $source_type" + wpctl set-default "$source_id" + arecord_opts -D pipewire /dev/null -q & + arecord_num=$((arecord_num+1)) + done +} + +main() +{ + start_test + logger_disabled || func_lib_start_log_collect + + setup_kernel_check_point + func_lib_check_sudo + func_pipeline_export "$tplg" "type:any" + + aplay_num=0 + arecord_num=0 + + if [ "$SOF_TEST_PIPEWIRE" == true ]; then + + dlogi "Running aplays" + run_aplays "${PIPEWIRE_OUTPUTS[@]}" + dlogi "Running arecords" + run_arecords "${PIPEWIRE_INPUTS[@]}" + + if [ "$aplay_num" == 0 ] && [ "$arecord_num" == 0 ]; then + skip_test "No sinks/sources to be tested found, skipping test" + fi -# Enable performance analysis -# shellcheck disable=SC2034 -DO_PERF_ANALYSIS=1 + else + + for idx in $(seq 0 $((PIPELINE_COUNT - 1))) + do + channel=$(func_pipeline_parse_value "$idx" channel) + rate=$(func_pipeline_parse_value "$idx" rate) + dev=$(func_pipeline_parse_value "$idx" dev) + pcm=$(func_pipeline_parse_value "$idx" pcm) + type=$(func_pipeline_parse_value "$idx" type) + + # Currently, copier will convert bit depth to S32_LE despite what bit depth + # is used in aplay, so make S32_LE as base bit depth for performance analysis. + fmt=S32_LE + + dlogi "Running (PCM: $pcm [$dev]<$type>) in background" + if [ "$type" == "playback" ]; then + aplay_opts -D "$dev" -c "$channel" -r "$rate" -f "$fmt" -d "$duration" /dev/zero -q & + aplay_num=$((aplay_num+1)) + else + arecord_opts -D "$dev" -c "$channel" -r "$rate" -f "$fmt" -d "$duration" /dev/null -q & + arecord_num=$((arecord_num+1)) + fi + done + fi + + sleep 1 # waiting stable streaming of aplay/arecord + dlogi "Number of aplay/arecord process started: $aplay_num, $arecord_num" + + real_aplay_num=$(ps --no-headers -C aplay | wc -l) + real_arecord_num=$(ps --no-headers -C arecord | wc -l) + if [ "$real_aplay_num" != "$aplay_num" ] || [ "$real_arecord_num" != "$arecord_num" ]; + then + dlogi "Number of aplay/arecord process running: $real_aplay_num, $real_arecord_num" + die "aplay/arecord process exit unexpectedly" + fi + + dlogi "Waiting for aplay/arecord process to exit" + sleep $((duration + 2)) + + # Enable performance analysis + # shellcheck disable=SC2034 + DO_PERF_ANALYSIS=1 +} + +{ + main "$@"; exit "$?" +} diff --git a/test-case/pipewire-wrapper.sh b/test-case/pipewire-wrapper.sh new file mode 100755 index 00000000..ba01729c --- /dev/null +++ b/test-case/pipewire-wrapper.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +## +## Case Name: Wrapper to run a test case given with Pipewire in setup that cannot set the environment variable. +## Keep this script as simple as possible and avoid additional layers of indirections when possible. +## Preconditions: +## Pipewire and Wireplumber are installed. +## Description: +## This script serves as a wrapper to execute a test case script using Pipewire. +## It expects the test case script file name (without path) as the first parameter, +## followed by other parameters required for that test case. +## Case step: +## 1. SOF_TEST_PIPEWIRE environment variable is set to true. +## 2. The test case script is executed. +## Expected result: +## The test case script is executed using Pipewire. + +set -e + +# Ensure the test case script file name is provided +if [ -z "$1" ]; then + echo "Error: No test case script file name provided. Exiting..." + exit 1 +fi + +export SOF_TEST_PIPEWIRE=true + +TESTDIR=$(realpath -e "$(dirname "${BASH_SOURCE[0]}")/..") + +# shellcheck disable=SC2145 +[ -x "$TESTDIR/test-case/$(basename "$1")" ] && exec "$TESTDIR"/test-case/"$@"