nixos/shared/modules/services/motu-m4-combined.nix

134 lines
5.3 KiB
Nix

{ config, lib, pkgs, ... }:
let
cfg = config.services.motu-m4-combined;
in
{
options.services.motu-m4-combined = {
enable = lib.mkEnableOption "MOTU M4 combined mono input";
user = lib.mkOption {
type = lib.types.str;
description = "User to run the setup service as";
};
devicePrefix = lib.mkOption {
type = lib.types.str;
default = "alsa_input.usb-MOTU_M4_M4MA0824DV-00.HiFi";
description = "Device prefix for MOTU M4 inputs";
};
virtualSourceName = lib.mkOption {
type = lib.types.str;
default = "motu_m4_combined";
description = "Name of the virtual combined source";
};
virtualSourceDescription = lib.mkOption {
type = lib.types.str;
default = "MOTU M4 All Inputs (Mono)";
description = "Human-readable description of the virtual source";
};
latencyMs = lib.mkOption {
type = lib.types.int;
default = 20;
description = "Loopback latency in milliseconds";
};
};
config = lib.mkIf cfg.enable {
# Create loopbacks and remap source via systemd user service
systemd.user.services.motu-m4-combined-setup = {
description = "Setup MOTU M4 combined mono input loopbacks";
after = [ "pipewire-pulse.service" ];
requires = [ "pipewire-pulse.service" ];
wantedBy = [ "pipewire-pulse.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = pkgs.writeShellScript "motu-m4-setup" ''
# Wait for pipewire-pulse to be fully ready
sleep 2
# Create null sink for mixing (internal, not visible in normal output lists)
${pkgs.pulseaudio}/bin/pactl load-module module-null-sink \
sink_name=motu_mixer \
sink_properties=device.description="MOTU_Mixer_(Internal)" \
channels=1 \
channel_map=mono
# Create loopbacks from each M4 input to the mixer (fail gracefully if M4 not connected)
${pkgs.pulseaudio}/bin/pactl load-module module-loopback \
source=${cfg.devicePrefix}__Mic1__source \
sink=motu_mixer channels=1 latency_msec=${toString cfg.latencyMs} \
source_dont_move=true sink_dont_move=true || true
${pkgs.pulseaudio}/bin/pactl load-module module-loopback \
source=${cfg.devicePrefix}__Mic2__source \
sink=motu_mixer channels=1 latency_msec=${toString cfg.latencyMs} \
source_dont_move=true sink_dont_move=true || true
${pkgs.pulseaudio}/bin/pactl load-module module-loopback \
source=${cfg.devicePrefix}__Line3__source \
sink=motu_mixer channels=1 latency_msec=${toString cfg.latencyMs} \
source_dont_move=true sink_dont_move=true || true
${pkgs.pulseaudio}/bin/pactl load-module module-loopback \
source=${cfg.devicePrefix}__Line4__source \
sink=motu_mixer channels=1 latency_msec=${toString cfg.latencyMs} \
source_dont_move=true sink_dont_move=true || true
# Create the virtual source from the mixer monitor
${pkgs.pulseaudio}/bin/pactl load-module module-remap-source \
master=motu_mixer.monitor \
source_name=${cfg.virtualSourceName} \
source_properties=device.description="${cfg.virtualSourceDescription}" \
channels=1 \
channel_map=mono
'';
# Clean up modules on service stop/restart
ExecStop = pkgs.writeShellScript "motu-m4-teardown" ''
# Unload loopback modules connected to motu_mixer
${pkgs.pulseaudio}/bin/pactl list modules short | \
${pkgs.gnugrep}/bin/grep "module-loopback.*sink=motu_mixer" | \
${pkgs.coreutils}/bin/cut -f1 | \
while read id; do
${pkgs.pulseaudio}/bin/pactl unload-module "$id" 2>/dev/null || true
done
# Unload the remap source module
${pkgs.pulseaudio}/bin/pactl list modules short | \
${pkgs.gnugrep}/bin/grep "module-remap-source.*source_name=${cfg.virtualSourceName}" | \
${pkgs.coreutils}/bin/cut -f1 | \
while read id; do
${pkgs.pulseaudio}/bin/pactl unload-module "$id" 2>/dev/null || true
done
# Unload the null sink module
${pkgs.pulseaudio}/bin/pactl list modules short | \
${pkgs.gnugrep}/bin/grep "module-null-sink.*sink_name=motu_mixer" | \
${pkgs.coreutils}/bin/cut -f1 | \
while read id; do
${pkgs.pulseaudio}/bin/pactl unload-module "$id" 2>/dev/null || true
done
'';
# Failsafe: ensure cleanup happens even if ExecStop fails
ExecStopPost = pkgs.writeShellScript "motu-m4-cleanup-failsafe" ''
# Cleanup any remaining MOTU-related modules (loopbacks, remap source, and null sink)
${pkgs.pulseaudio}/bin/pactl list modules short | \
${pkgs.gnugrep}/bin/grep -E "module-loopback.*motu_mixer|module-remap-source.*${cfg.virtualSourceName}|module-null-sink.*motu_mixer" | \
${pkgs.coreutils}/bin/cut -f1 | \
while read id; do
${pkgs.pulseaudio}/bin/pactl unload-module "$id" 2>/dev/null || true
done
'';
};
};
};
}