179 lines
7.1 KiB
Nix
179 lines
7.1 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";
|
|
};
|
|
|
|
usbVendorId = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "07fd";
|
|
description = "USB Vendor ID for MOTU M4 device";
|
|
};
|
|
|
|
usbProductId = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "000b";
|
|
description = "USB Product ID for MOTU M4 device";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
# Udev rule to tag MOTU M4 device for systemd tracking
|
|
services.udev.extraRules = ''
|
|
# Tag MOTU M4 device so systemd can track it
|
|
SUBSYSTEM=="usb", ATTR{idVendor}=="${cfg.usbVendorId}", ATTR{idProduct}=="${cfg.usbProductId}", TAG+="systemd"
|
|
'';
|
|
|
|
# Create loopbacks and remap source via systemd user service
|
|
systemd.user.services.motu-m4-combined-setup = {
|
|
description = "Setup MOTU M4 combined mono input loopbacks";
|
|
# Bind to MOTU M4 USB device - service only runs when device is connected
|
|
bindsTo = [ "dev-serial-by\\x2did-usb\\x2dMOTU_M4_M4MA0824DV\\x2dif05.device" ];
|
|
after = [
|
|
"pipewire-pulse.service"
|
|
"dev-serial-by\\x2did-usb\\x2dMOTU_M4_M4MA0824DV\\x2dif05.device"
|
|
];
|
|
requires = [ "pipewire-pulse.service" ];
|
|
wantedBy = [ "dev-serial-by\\x2did-usb\\x2dMOTU_M4_M4MA0824DV\\x2dif05.device" ];
|
|
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
|
|
# Verify MOTU M4 device is actually available before starting
|
|
ExecCondition = pkgs.writeShellScript "motu-m4-check-device" ''
|
|
# Check if MOTU M4 USB device is present
|
|
if ! ${pkgs.usbutils}/bin/lsusb -d ${cfg.usbVendorId}:${cfg.usbProductId} > /dev/null 2>&1; then
|
|
echo "MOTU M4 USB device not found (${cfg.usbVendorId}:${cfg.usbProductId})"
|
|
exit 1
|
|
fi
|
|
|
|
# Wait a moment for audio subsystem to fully enumerate the device
|
|
sleep 1
|
|
|
|
# Check if at least one MOTU audio source is available in PipeWire
|
|
if ! ${pkgs.pulseaudio}/bin/pactl list sources short | ${pkgs.gnugrep}/bin/grep -q "${cfg.devicePrefix}"; then
|
|
echo "MOTU M4 audio sources not yet available in PipeWire"
|
|
exit 1
|
|
fi
|
|
|
|
echo "MOTU M4 device detected and ready"
|
|
exit 0
|
|
'';
|
|
|
|
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
|
|
# Device presence is guaranteed by ExecCondition check
|
|
${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
|
|
|
|
${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
|
|
|
|
${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
|
|
|
|
${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
|
|
|
|
# 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
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
}
|