2

I understand that you could select a profile for an audio device using pactl set-card-profile CARD PROFILE. But pactl is a PulseAudio utility, meaning it adds an unnecessary layer and of course it requires you to install PulseAudio to some degree.

I'd like to know what the native way of setting audio device profiles is with PipeWire.

I found a PipeWire utility called pw-cli which seems like it might be what I need, but it's very unclear to me how it is used correctly. pw-cli [options] [command] -h, --help Show this help --version Show version -d, --daemon Start as daemon (Default false) -r, --remote Remote daemon name

Available commands:
    help                    Show this help
    load-module             Load a module. <module-name> [<module-arguments>]
    unload-module           Unload a module. <module-var>
    connect                 Connect to a remote. [<remote-name>]
    disconnect              Disconnect from a remote. [<remote-var>]
    list-remotes            List connected remotes.
    switch-remote           Switch between current remotes. [<remote-var>]
    list-objects            List objects or current remote. [<interface>]
    info                    Get info about an object. <object-id>|all
    create-device           Create a device from a factory. <factory-name> [<properties>]
    create-node             Create a node from a factory. <factory-name> [<properties>]
    destroy                 Destroy a global object. <object-id>
    create-link             Create a link between nodes. <node-id> <port-id> <node-id> <port-id> [<properties>]
    export-node             Export a local node to the current remote. <node-id> [remote-var]
    enum-params             Enumerate params of an object <object-id> <param-id>
    set-param               Set param of an object <object-id> <param-id> <param-json>
    permissions             Set permissions for a client <client-id> <object> <permission>
    get-permissions         Get permissions of a client <client-id>
    send-command            Send a command <object-id>
    dump                    Dump objects in ways that are cleaner for humans to understand [short|deep|resolve|notype] [-sdrt] [all|Core|Module|Device|Node|Port|Factory|Client|Link|Session|Endpoint|EndpointStream|<id>]
    quit                    Quit

I can call enum-params for Route and Profile without any errors, but the output is very cryptic and hard to parse. But both of these commands provide expected output:

pw-cli enum-params 45 Route
pw-cli enum-params 45 Profile

For retrieving data pw-dump appears to make way more sense because its output format is JSON. I wrote a bunch of helper functions (get_audio_devices, get_object_by_id, get_device_by_name, get_active_profiles, get_active_routes, get_all_profiles, get_all_routes, get_nodes_for_device_by_id) to get all information I would need in order to pass the required data to pw-cli set-param. And then I wrote two more functions (set_profile and set_route) which wrap around pw-cli and make it more reasonable to use.

These are my functions:

#!/bin/bash

# Function: get_audio_devices
# Description: Get all audio devices as a JSON array of objects
function get_audio_devices() {
    pw-dump | jq -r '.[] | select(.type == "PipeWire:Interface:Device" and .info.props."media.class" == "Audio/Device")'
}

# Function: get_object_by_id
# Description: Get a object by object ID as a JSON object
# Parameters:
#   - $1: The object ID of the device, node, module, factory, port, link or whatever
function get_object_by_id() {
    local object_id="$1"
    pw-dump | jq -r ".[] | select(.id == $object_id)"
}

# Function: get_device_by_name
# Description: Get a device by name as a JSON object
# Parameters:
#   - $1: The name of the device
function get_device_by_name() {
    local device_name="$1"
    pw-dump | jq -r ".[] | select(.info.props.\"device.name\" == \"$device_name\")"
}

# Function: get_active_profiles
# Description: Get active profiles for a device by object ID as a JSON array of objects
# Parameters:
#   - $1: The object ID of the device
function get_active_profiles() {
    local device_object_id="$1"
    pw-dump | jq -r ".[] | select(.id == $device_object_id) | .info.params.Profile"
}

# Function: get_active_routes
# Description: Get active routes for all nodes of the active profile(s) of a device by object ID as a JSON array of objects
# Parameters:
#   - $1: The object ID of the device
function get_active_routes() {
    local device_object_id="$1"
    pw-dump | jq -r ".[] | select(.id == $device_object_id) | .info.params.Route"
}

# Function: get_all_profiles
# Description: Get all profiles for a device by object ID as a JSON array of objects
# Parameters:
#   - $1: The object ID of the device
function get_all_profiles() {
    local device_object_id="$1"
    pw-dump | jq -r ".[] | select(.id == $device_object_id) | .info.params.EnumProfile"
}

# Function: get_all_routes
# Description: Get all routes for all nodes of a device by object ID as a JSON array of objects
# Parameters:
#   - $1: The object ID of the device
function get_all_routes() {
    local device_object_id="$1"
    pw-dump | jq -r ".[] | select(.id == $device_object_id) | .info.params.EnumRoute"
}

# Function: get_nodes_for_device_by_id
# Description: Get all nodes for a device by object ID as a JSON array of objects
# Parameters:
#   - $1: The object ID of the device
function get_nodes_for_device_by_id() {
    local device_object_id="$1"
    pw-dump | jq -r --argjson dev_id "$device_object_id" '[.[] | select(.type == "PipeWire:Interface:Node" and .info.props."device.id" == $dev_id)]'
    #pw-dump | jq -r '[.[] | select(.type == "PipeWire:Interface:Node" and .info.props."device.id" == "'"$device_object_id"'")]'
}

# Function: set_route
# Description: Set route for a given node by object ID
# Parameters:
#   - $1: The object ID of the node
#   - $2: The index of the new route
function set_route() {
    local node_object_id="$1"
    local route_index="$2"

    local node=$(get_object_by_id $node_object_id)
    local device_object_id=$(echo "$node" | jq -r '.info.props["device.id"]')

    # Get available route "templates" for the device's active profile(s)
    routes=$(get_all_routes $device_object_id)

    # Get the first route template (edit: using the fourth because the first 3 are for input rather than output)
    first_route_template=$(echo "$routes" | jq '.[3]')

    # Create a Routes entry for the given node based on the given template and save as new JSON object
    route_to_set=$(echo "$first_route_template")

    # Set the "route.hw-mute" property to false beacuase I have no clu how to find out the right value
    route_to_set=$(echo "$route_to_set" | jq '.info += ["route.hw-mute", "true"]')

    # Set the "route.hw-volume" property to false beacuase I have no clu how to find out the right value
    route_to_set=$(echo "$route_to_set" | jq '.info += ["route.hw-volume", "true"]')

    # Calculate the length of the "info" array and set the first element accordingly
    info_length=$(echo "$route_to_set" | jq '.info | length')
    route_to_set=$(echo "$route_to_set" | jq ".info[0] = ($info_length - 1) / 2")

    # Get the index of the node of which we want to change the route
    node_index=$(echo "$node" | jq -r '.info.props["card.profile.device"]')

    # Set device property
    route_to_set=$(echo "$route_to_set" | jq ". + { \"device\": $node_index }")

    # Gather values for the properties of the "props" section 
    mute=$(echo "$node" | jq -r '.info.params.Props[0].mute')
    channel_volumes=$(echo "$node" | jq -r '.info.params.Props[0].channelVolumes')
    volume_base=$(echo "$node" | jq -r '.info.params.Props[0].volume')
    volume_step=0.000015 # No clue how to get the correct value
    channel_map=$(echo "$node" | jq -r '.info.params.Props[0].channelMap')
    soft_volumes=$(echo "$node" | jq -r '.info.params.Props[0].softVolumes')
    latency_offset=$(echo "$node" | jq -r '.info.params.Props[1].latencyOffsetNsec')

    # Set the properties in the "props" section
    route_to_set=$(echo "$route_to_set" | jq "
        .props += {
            \"mute\": $mute,
            \"channelVolumes\": $channel_volumes,
            \"volumeBase\": $volume_base,
            \"volumeStep\": $volume_step,
            \"channelMap\": $channel_map,
            \"softVolumes\": $soft_volumes,
            \"latencyOffsetNsec\": $latency_offset
        }"
    )

    # Get active profile
    profiles=$(get_active_profiles $device_object_id)
    first_active_profile=$(echo "$profiles" | jq '.[0]')

    # Get profile index
    first_active_profile_index=$(echo "$first_active_profile" | jq -r '.index')
    route_to_set=$(echo "$route_to_set" | jq ". + { \"profile\": $first_active_profile_index }")

    # Add the "save" property to the "route_to_set" object
    route_to_set=$(echo "$route_to_set" | jq '. + { "save": false }')

    # Get active routes
    old_active_routes="$(get_active_routes $device_object_id)"

    # Get route index
    route_index=$(echo "$old_active_routes" | jq -r "map(.device == $node_index) | index(true)")

    # Create a new routes array where the route we want to set replaces the old one
    updated_active_routes=$(echo "$old_active_routes" | jq ".[$route_index] = $route_to_set")

    # Check diff between old and new routes array
    #file1=/tmp/updated_active_routes && file2=/tmp/old_active_routes && echo "$updated_active_routes" > "$file1" && echo "$old_active_routes" > "$file2" && meld "$file1" "$file2" ; rm "$file1" "$file2"
    
    # Set the updated routes
    pw-cli set-param $device_object_id Route "$updated_active_routes"
}

# Function: set_profile
# Description: Set route for a given device by object ID
# Parameters:
#   - $1: The object ID of the device
#   - $2: The index of the new profile
function set_profile() {
    local device_object_id="$1"
    local profile_index="$2"

    # Get available profile "templates" for device with object ID 78
    profiles="$(get_all_profiles $device_object_id)"

    # Get desired profile template
    profile_template=$(echo "$profiles" | jq ".[] | select(.index == $PROFILE_INDEX)")

    # Add the "save" property and save as new JSON object
    profile_to_set=$(echo "$profile_template" | jq '. + { "save": false }')

    # Set the new profile(s)
    pw-cli set-param $device_object_id Profile "[ $profile_to_set ]"
}

# Example usage:
# get_audio_devices
# get_object_by_id 78
# get_device_by_name alsa_card.pci-0000_00_1f.3
# get_active_profiles 78
# get_active_routes 78
# get_all_profiles 78
# get_all_routes 78
# get_nodes_for_device_by_id 78
# set_profile 78 1
# set_route 45 0

But there is some guess work involved. I've been trying to use set_profile and set_routes without any success.
My attempt to change the profile:

#####################################################
# Attempt to change the profile                     #
#####################################################

# My audio device
DEVICE_NAME="alsa_card.pci-0000_00_1f.3"

# Index of the profile I want to use
PROFILE_INDEX=1

# Get device id
device_object_id=$(get_device_by_name "$DEVICE_NAME" | jq -r '.id')

# Set the desired profile
set_profile $device_object_id $PROFILE_INDEX
# Doesn't work and results in:
#    Array: child.size 2, child.type Spa:String
#    String "{"
#    String ""
#    String ""
#    String ""
#    String ""
#    String ""
#    String ""
#    String ""
#####################################################

My attempt to change the route:

#####################################################
# Attempt to change the route                       #
#####################################################

# My audio device
DEVICE_NAME="alsa_card.pci-0000_00_1f.3"

# Get device id
device_object_id=$(get_device_by_name "$DEVICE_NAME" | jq -r '.id')

# Get all nodes for the device
nodes=$(get_nodes_for_device_by_id $device_object_id)

# Error if there are no nodes
if [ "$nodes" == "[]" ]; then
    echo "There are no nodes for the active profile(s) of device with name '$DEVICE_NAME' (object ID: $device_object_id)" >&2
    exit 1
fi

# Get first sink node of current profile
first_sink_node_object_id=$(echo "$nodes" | jq '.[] | select(.info.props."media.class" == "Audio/Sink")'| jq -r '.id')

# Get first route output route (it's not guaranteed that this route is available for that specific node and profile)
first_output_route_index=$(get_all_routes $device_object_id | jq '.[] | select(.direction == "Output") | .index' | head -n 1)

# Set the route
set_route $first_sink_node_object_id $first_output_route_index
# Doesn't work and results in:
#    Array: child.size 2, child.type Spa:String
#    String "{"
#    String ""
#    String ""
#    String ""
#    String ""
#    String ""
#    String ""
#    String ""
#####################################################

Now let me elaborate on what I've been trying to do. The signature for set-params is as follows:

pw-cli set-param <object-id> <param-id> <param-json>

The object-id is the device id and the param-id is Route and Profile respectively.

My first problem is that you can't just pass the id or index of the route/profile you want to set, but you have to pass entire JSON objects and it's not obvious what they have to look like.

It appears that every device has a set of possible Profiles and Routes. They can be found as an array of objects in .info.params.EnumProfile and .info.params.EnumRoute respectively. From what I can tell these objects are templates. And the active profiles and routes are stored in .info.params.Profile and .info.params.Route respectively. They look identical to the templates at first glance, but they have additional properties. Every object in .info.params.Profile has an additional property "save" which is a boolean that can be true or false. For the .info.params.Route array it is way worse it has additional properties device, profile and save and additional items in one of its properties called info and there is a new property "props" which is an object with properties mute, channelVolumes, volumeBase, volumeStep, channelMap, softVolumes and latencyOffsetNsec. I have no clue where I would get the correct values for these properties. It seems like some of these properties can be retrieved from the associated (sink/source)-node (the channelMap and softVolumes for example), but for some of the properties I'm just guessing what values I should set.

Another problem is that I don't understand why .info.params.Profile is an array. This is suggesting that there can be multiple active profiles, but then how do I know if the device supports that etc?

Any help would be appreciated.

Forivin
  • 779
  • 5
  • 18
  • 37

0 Answers0