PipeWire: Difference between revisions

From NixOS Wiki
imported>Kittywitch
m Fixed syntax error in PulseAudio backend
imported>Fufexan
Change examples to a better "drop-in" config
Line 146: Line 146:
   config.pipewire = {
   config.pipewire = {
     "context.properties" = {
     "context.properties" = {
       "default.clock.min-quantum" = 32; # default; going lower may cause crackles and distorted audio
      "link.max-buffers" = 16;
      "log.level" = 2;
      "default.clock.rate" = 48000;
      "default.clock.quantum" = 32;
       "default.clock.min-quantum" = 32;
      "default.clock.max-quantum" = 32;
      "core.daemon" = true;
      "core.name" = "pipewire-0";
     };
     };
    "context.modules" = [
      {
        name = "libpipewire-module-rtkit";
        args = {
          "nice.level" = -15;
          "rt.prio" = 88;
          "rt.time.soft" = 200000;
          "rt.time.hard" = 200000;
        };
        flags = [ "ifexists" "nofail" ];
      }
      { name = "libpipewire-module-protocol-native"; }
      { name = "libpipewire-module-profiler"; }
      { name = "libpipewire-module-metadata"; }
      { name = "libpipewire-module-spa-device-factory"; }
      { name = "libpipewire-module-spa-node-factory"; }
      { name = "libpipewire-module-client-node"; }
      { name = "libpipewire-module-client-device"; }
      {
        name = "libpipewire-module-portal";
        flags = [ "ifexists" "nofail" ];
      }
      {
        name = "libpipewire-module-access";
        args = {};
      }
      { name = "libpipewire-module-adapter"; }
      { name = "libpipewire-module-link-factory"; }
      { name = "libpipewire-module-session-manager"; }
    ];
   };
   };
};
};
Line 159: Line 196:
services.pipewire = {
services.pipewire = {
   config.pipewire-pulse = {
   config.pipewire-pulse = {
    "context.properties" = {
      "log.level" = 2;
    };
     "context.modules" = [
     "context.modules" = [
      {
        name = "libpipewire-module-rtkit";
        args = {
          "nice.level" = -15;
          "rt.prio" = 88;
          "rt.time.soft" = 200000;
          "rt.time.hard" = 200000;
        };
        flags = [ "ifexists" "nofail" ];
      }
      { name = "libpipewire-module-protocol-native"; }
      { name = "libpipewire-module-client-node"; }
      { name = "libpipewire-module-adapter"; }
      { name = "libpipewire-module-metadata"; }
       {
       {
         name = "libpipewire-module-protocol-pulse";
         name = "libpipewire-module-protocol-pulse";
         args = {
         args = {
           "pulse.min.quantum" = 32; # controls minimum playback quant
           "pulse.min.req" = "32/48000";
           "pulse.min.req" = 32; # controls minimum recording quant
          "pulse.default.req" = "32/48000";
           "pulse.min.frag" = 32; # controls minimum fragment size
           "pulse.max.req" = "32/48000";
           "server.address" = [ "unix:native" ]; # the default address of the server
           "pulse.min.quantum" = "32/48000";
          "pulse.max.quantum" = "32/48000";
           "server.address" = [ "unix:native" ];
         };
         };
       }
       }
     ];
     ];
    "stream.properties" = {
      "node.latency" = "32/48000";
      "resample.quality" = 1;
    };
   };
   };
};
};
Line 184: Line 244:
         actions = {
         actions = {
           update-props = {
           update-props = {
             "audio.format" = "S16LE";
             "audio.format" = "S32LE";
             "audio.rate" = 48000;
             "audio.rate" = 96000; # for USB soundcards it should be twice your desired rate
             "api.alsa.period-size" = 160; # defaults to 1024, tweak by trial-and-error
             "api.alsa.period-size" = 32; # defaults to 1024, tweak by trial-and-error
             #"api.alsa.disable-batch" = true; # generally, USB soundcards use the batch mode
             #"api.alsa.disable-batch" = true; # generally, USB soundcards use the batch mode
           };
           };

Revision as of 19:26, 6 May 2021

PipeWire is a new low-level multimedia framework. It aims to offer capture and playback for both audio and video with minimal latency and support for PulseAudio-, JACK-, ALSA- and GStreamer-based applications.

The daemon based on the framework can be configured to be both an audio server (with PulseAudio and JACK features) and a video capture server.

PipeWire also supports containers like Flatpak and does not rely on audio and video user groups but rather it uses a Polkit-like security model asking Flatpak or Wayland for permission to record screen or audio.

Enabling PipeWire

Add to your configuration:

# Remove sound.enable or turn it off if you had it set previously, it seems to cause conflicts with pipewire
#sound.enable = false;

# rtkit is optional but recommended
security.rtkit.enable = true;
services.pipewire = {
  enable = true;
  alsa.enable = true;
  alsa.support32Bit = true;
  pulse.enable = true;
  # If you want to use JACK applications, uncomment this
  #jack.enable = true;

  # use the example session manager (no others are packaged yet so this is enabled by default,
  # no need to redefine it in your config for now)
  #media-session.enable = true;
};

Some useful knobs if you want to finetune or debug your setup:

services.pipewire = {
  config.pipewire = {
    "context.properties" = {
      #"link.max-buffers" = 64;
      "link.max-buffers" = 16; # version < 3 clients can't handle more than this
      "log.level" = 2; # https://docs.pipewire.org/#Logging
      #"default.clock.rate" = 48000;
      #"default.clock.quantum" = 1024;
      #"default.clock.min-quantum" = 32;
      #"default.clock.max-quantum" = 8192;
  };
};

NOTE: Arrays are replaced rather than merged with defaults, so in order to keep any default items in the configuration, they have to be listed.

Bluetooth Configuration

PipeWire handles Bluetooth very well, and it can be configured to use specific codecs. The mSBC codec provides slightly better sound quality in calls than regular HFP/HSP.

services.pipewire  = {
  media-session.config.bluez-monitor.rules = [
    {
      # Matches all cards
      matches = [ { "device.name" = "~bluez_card.*"; } ];
      actions = {
        "update-props" = {
          "bluez5.reconnect-profiles" = [ "hfp_hf" "hsp_hs" "a2dp_sink" ];
          # mSBC is not expected to work on all headset + adapter combinations.
          "bluez5.msbc-support" = true;
        };
      };
    }
    {
      matches = [
        # Matches all sources
        { "node.name" = "~bluez_input.*"; }
        # Matches all outputs
        { "node.name" = "~bluez_output.*"; }
      ];
      actions = {
        "node.pause-on-idle" = false;
      };
    }
  ];
};

Advanced Configuration

PipeWire can be extensively configured to fit the users' needs. Should the user want to do some fancy routing with null sinks, these can be defined directly in the config as shown below.

This is especially convenient if the user has a multi-channel (8+, or something "weird" like 2x2, 3x2) soundcard that keeps confusing applications with too many channels or a bad channel layout.

Note: those cards can be set to the "Pro Audio" profile with pavucontrol so PipeWire doesn't try to guess a wrong channel layout for them.

services.pipewire = {
  config.pipewire = {
    "context.objects" = [
      {
        # A default dummy driver. This handles nodes marked with the "node.always-driver"
        # properyty when no other driver is currently active. JACK clients need this.
        factory = "spa-node-factory";
        args = {
          "factory.name"     = "support.node.driver";
          "node.name"        = "Dummy-Driver";
          "priority.driver"  = 8000;
        };
      }
      {
        factory = "adapter";
        args = {
          "factory.name"     = "support.null-audio-sink";
          "node.name"        = "Microphone-Proxy";
          "node.description" = "Microphone";
          "media.class"      = "Audio/Source/Virtual";
          "audio.position"   = "MONO";
        };
      }
      {
        factory = "adapter";
        args = {
          "factory.name"     = "support.null-audio-sink";
          "node.name"        = "Main-Output-Proxy";
          "node.description" = "Main Output";
          "media.class"      = "Audio/Sink";
          "audio.position"   = "FL,FR";
        };
      }
    ];
  };
};

Linking nodes

The config does not currently cover linking nodes together, but this can be fixed with a script. Soundcard names and ports should be replaced with the ones from the user's configuration:

#!/usr/bin/env bash

# ports obtained from `pw-link -io`

pw-link "Main-Output-Proxy:monitor_FL" "alsa_output.usb-Native_Instruments_Komplete_Audio_6_69BC86B9-00.pro-audio:playback_1"
pw-link "Main-Output-Proxy:monitor_FR" "alsa_output.usb-Native_Instruments_Komplete_Audio_6_69BC86B9-00.pro-audio:playback_2"

pw-link "alsa_input.usb-M-Audio_Fast_Track-00.pro-audio:capture_1" "Microphone-Proxy:input_MONO"

In order to load the script on startup, it can be added to ~/.xprofile or the specific DE/WM autostart config. Similarly, a one-shot user service can be created that runs the script.

Low-latency setup

Audio production and rhythm games require lower latency audio than general applications. PipeWire can achieve the required latency with much less CPU usage compared to PulseAudio, with the appropriate configuration. The minimum period size controls how small a buffer can be. The lower it is, the less latency there is. PipeWire has a value of 32 by default, which amounts to 1.33ms. It can be brought lower if needed:

services.pipewire = {
  config.pipewire = {
    "context.properties" = {
      "link.max-buffers" = 16;
      "log.level" = 2;
      "default.clock.rate" = 48000;
      "default.clock.quantum" = 32;
      "default.clock.min-quantum" = 32;
      "default.clock.max-quantum" = 32;
      "core.daemon" = true;
      "core.name" = "pipewire-0";
    };
    "context.modules" = [
      {
        name = "libpipewire-module-rtkit";
        args = {
          "nice.level" = -15;
          "rt.prio" = 88;
          "rt.time.soft" = 200000;
          "rt.time.hard" = 200000;
        };
        flags = [ "ifexists" "nofail" ];
      }
      { name = "libpipewire-module-protocol-native"; }
      { name = "libpipewire-module-profiler"; }
      { name = "libpipewire-module-metadata"; }
      { name = "libpipewire-module-spa-device-factory"; }
      { name = "libpipewire-module-spa-node-factory"; }
      { name = "libpipewire-module-client-node"; }
      { name = "libpipewire-module-client-device"; }
      {
        name = "libpipewire-module-portal";
        flags = [ "ifexists" "nofail" ];
      }
      {
        name = "libpipewire-module-access";
        args = {};
      }
      { name = "libpipewire-module-adapter"; }
      { name = "libpipewire-module-link-factory"; }
      { name = "libpipewire-module-session-manager"; }
    ];
  };
};

NOTE: Every setup is different, and a lot of factors determine your final latency, like CPU speed, RT/PREEMPTIVE kernels and soundcards supporting different audio formats. That's why 32 isn't always a value that's going to work for everyone. The best way to get everything working is to keep increasing the quant value until you get no crackles (underruns) or until you get audio again (in case there wasn't any). This won't guarantee the lowest possible latency, but will provide a decent one paired with stable audio.

PulseAudio backend

Applications using the Pulse backend have a separate configuration. The default minimum value is 1024, so it needs to be tweaked if low-latency audio is desired.

services.pipewire = {
  config.pipewire-pulse = {
    "context.properties" = {
      "log.level" = 2;
    };
    "context.modules" = [
      {
        name = "libpipewire-module-rtkit";
        args = {
          "nice.level" = -15;
          "rt.prio" = 88;
          "rt.time.soft" = 200000;
          "rt.time.hard" = 200000;
        };
        flags = [ "ifexists" "nofail" ];
      }
      { name = "libpipewire-module-protocol-native"; }
      { name = "libpipewire-module-client-node"; }
      { name = "libpipewire-module-adapter"; }
      { name = "libpipewire-module-metadata"; }
      {
        name = "libpipewire-module-protocol-pulse";
        args = {
          "pulse.min.req" = "32/48000";
          "pulse.default.req" = "32/48000";
          "pulse.max.req" = "32/48000";
          "pulse.min.quantum" = "32/48000";
          "pulse.max.quantum" = "32/48000";
          "server.address" = [ "unix:native" ];
        };
      }
    ];
    "stream.properties" = {
      "node.latency" = "32/48000";
      "resample.quality" = 1;
    };
  };
};

As a general rule, the values in pipewire-pulse should not be lower than the ones in pipewire.

Controlling the ALSA devices

It is possible to configure various aspects of soundcards through PipeWire, including format, period size and batch mode:

services.pipewire = {
  media-session.config.alsa-monitor = {
    rules = [
      {
        matches = [ { "node.name" = "alsa_output.*" } ];
        actions = {
          update-props = {
            "audio.format" = "S32LE";
            "audio.rate" = 96000; # for USB soundcards it should be twice your desired rate
            "api.alsa.period-size" = 32; # defaults to 1024, tweak by trial-and-error
            #"api.alsa.disable-batch" = true; # generally, USB soundcards use the batch mode
          };
        };
      };
    ];
  };
};

The matches attribute applies the actions to the devices/properties listed there. It is usually used with soundcard names, like shown in the config above. <alsa_device> can be one of the outputs of

$ pw-dump | grep node.name | grep alsa

See also