{ 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 ''; }; }; }; }