Hass remote speakers with MPD - NixOS

August 20, 2023

This is part of a series of posts on using MPD to set up remote speakers controlled by HomeAssistant on various devices. This post covers NixOS; specifically, a home theatre PC (pladix) running NixOS and connected to speakers via HDMI.

I expected this to be the easy one, but the migration to Pipewire has complicated things: mpd runs as a system user, but pipewire runs as a per-user service and system users don’t get a copy of it. For headless machines you can probably get away with just turning on services.pipewire.systemWide and calling it a day, but on headed systems this breaks a bunch of things, like user-accessible volume controls.

The quick and dirty approach is to run mpd as one of the login users who has pipewire enabled:

services.pipewire.enable = true;
services.mpd = {
  enable = true;
  network.listenAddress = "any";
  user = "htpc";
  extraConfig = ''
    default_permissions "read,add,control"
    restore_paused "yes"
    input {
      plugin "curl"
    }
    audio_output {
      type "pipewire"
      name "local pipewire output"
    }
  '';
};
# So that it knows where to find the pipewire control socket
systemd.services.mpd.environment.XDG_RUNTIME_DIR =
  "/run/user/${config.users.users.htpc.uid}";

A somewhat less dirty approach that I’m still not entirely happy with is to enable support for the PulseAudio TCP protocol in pipewire:, and have mpd connect to the tcp socket:

services.pipewire = { enable = true; pulse.enable = true; };
environment.etc."pipewire/pipewire-pulse.conf.d/99-pulse-tcp.conf".text = ''
  pulse.properties = {
    server.address = [
      "unix:native"
      {
        address = "tcp:127.0.0.1:4713"
        client.access = "allowed"
      }
    ]
  }
'';

And having done this, you can tell mpd to connect to it by changing the audio_output block thus:

audio_output {
  type "pulse"
  name "pipewire pulseaudio emulation"
  server "tcp:127.0.0.1:4713"
}

This gives user isolation between mpd and the seated user, but if the seated user logs out, the pipewire backend goes with them; you might want to use loginctl enable-linger on that user to stop that from happening, depending on your use case. (In my case the HTPC automatically logs in as that user on startup and basically never logs out, so it’s a non-issue.)

Keeping the output awake

Rather than using its internal speakers, pladix is connected via HDMI to an A/V receiver. However, when not playing audio, the receiver goes to sleep very quickly – within five seconds – and since it takes several seconds to wake back up, this means long announcements lose the first few words and short effects aren’t audible at all.

Conveniently, it turns out that pipewire’s “session manager”, wireplumber, has options that solve this:

environment.etc."wireplumber/main.lua.d/99-disable-suspend.lua".text = ''
  table.insert(alsa_monitor.rules,
    {
      matches = };
      apply_properties = {
        -- Don't ever suspend this output
        ["node.pause-on-idle"] = false;
        ["session.suspend-timeout-seconds"] = 0;
        -- constantly wiggle the lowest bit of the audio output to keep
        -- the amp from deciding it can sleep
        ["dither.noise"] = 1;
      }
    }
  )
'';

Depending on your specific hardware, you might only need one or two of those options. The first two, in particular, tell Pipewire never to “sleep” the output, which for a lot of hardware is probably sufficient. The last one tells it to constantly (but inaudibly) vary the output, so that receivers that go to sleep even when the output is active if it’s “playing” silence will have to stay awake.

After doing this, it still won’t wake up until the first sound is played after a system reboot, but afterwards it will stay awake indefinitely.