10 KiB
MOTU M4 Combined Mono Input Module
Purpose
Creates a virtual mono audio source that combines all 4 inputs from a MOTU M4 audio interface into a single input device. This is useful for:
- Video conferencing (Zoom, Discord, etc.) where you want all inputs mixed together
- Recording scenarios where you need a simple mono mix
- Applications that don't support multi-input selection
Architecture
Components
- Null Sink (
motu_mixer): Internal PulseAudio/PipeWire sink that receives audio from all inputs - 4 Loopback Modules: Route each M4 input → null sink
- Mic In 1L (Left)
- Mic In 2R (Right)
- Line In 3L (Left)
- Line In 4R (Right)
- Remap Source Module: Exposes
motu_mixer.monitoras the virtual "MOTU M4 All Inputs (Mono)" source
Audio Flow
M4 Mic In 1L ──┐
M4 Mic In 2R ──┤
M4 Line In 3L ─┼──> [motu_mixer] ──> [monitor] ──> "MOTU M4 All Inputs (Mono)"
M4 Line In 4R ─┘ (null sink) (virtual source)
Important: The null sink is internal only - no audio is output to physical devices. The M4's built-in hardware monitoring is used instead.
Device Detection & Auto-Start/Stop
The module is device-aware and only runs when the MOTU M4 is physically connected:
- Auto-Start: When you plug in the MOTU M4, the systemd service automatically starts
- Auto-Stop: When you unplug the MOTU M4, the service automatically stops and cleans up
- Boot Handling: Works correctly whether M4 is connected at boot time or not
- No Phantom Sources: The virtual source only appears when the hardware is actually present
This is achieved through:
- Udev tagging: USB device is tagged for systemd tracking
- Device binding: Service uses
bindsTo=to tie its lifecycle to the USB device - ExecCondition: Additional validation ensures PipeWire has enumerated the audio sources before setup
Configuration Options
services.motu-m4-combined = {
enable = true; # Enable the module
user = "username"; # User to run the service as
devicePrefix = "alsa_input.usb-MOTU_M4_M4MA0824DV-00.HiFi"; # M4 device prefix
virtualSourceName = "motu_m4_combined"; # Virtual source name
virtualSourceDescription = "MOTU M4 All Inputs (Mono)"; # Description shown in apps
latencyMs = 20; # Loopback latency in milliseconds
usbVendorId = "07fd"; # USB Vendor ID (Mark of the Unicorn)
usbProductId = "000b"; # USB Product ID (M Series/M4)
};
Implementation Details
Why Systemd Service Instead of PipeWire Config?
Decision: Use systemd user service to create all modules instead of PipeWire declarative config.
Rationale:
- PipeWire config limitations: The
services.pipewire.extraConfig.pipewireonly works for property-based configuration. It cannot reliably createcontext.objects(null sinks) or load PulseAudio compatibility modules. - Portability: Self-contained service works on any NixOS system with PipeWire, no special PipeWire configuration needed.
- Guaranteed ordering: Null sink is created before loopbacks try to connect to it.
- Easier debugging: Everything in one service, one log stream to check.
Reference: NixOS PipeWire Wiki - Null Sinks suggests scripts for tasks not covered by declarative config.
Why All Modules in One Service?
Initial Problem: The null sink was created via PipeWire config, but it didn't actually get created. The loopback modules then auto-routed to the default sink (DX1 Headphones), causing unwanted monitoring.
Solution: Move everything into the systemd service:
- Create null sink FIRST
- Then create loopbacks (they now have a target)
- Finally create remap source
This ensures proper ordering and prevents audio from leaking to physical outputs.
Troubleshooting
Audio Playing Through Wrong Output
Symptom: You hear the combined input through your default audio output (headphones, speakers, etc.)
Cause: The motu_mixer null sink wasn't created, so loopbacks auto-routed to the default sink.
Fix:
# Check if null sink exists
pactl list sinks short | grep motu_mixer
# If missing, restart the service
systemctl --user restart motu-m4-combined-setup
# Check service logs
journalctl --user -u motu-m4-combined-setup -n 50
Combined Source Not Appearing
Symptom: "MOTU M4 All Inputs (Mono)" doesn't show up in audio applications.
Possible causes:
- M4 is not connected (most common - service won't start if device is absent)
- Service condition check failed (device present but audio sources not ready)
- Service failed to start
- PipeWire/WirePlumber not running
Fix:
# Check if MOTU M4 USB device is connected
lsusb | grep -i motu
# Check service status (will be inactive if device not connected)
systemctl --user status motu-m4-combined-setup
# Check if M4 is detected in audio system
wpctl status | grep -i motu
pactl list sources short | grep MOTU
# If device is connected but service isn't running, check logs
journalctl --user -u motu-m4-combined-setup -n 50
# Try restarting the service (will only work if device is connected)
systemctl --user restart motu-m4-combined-setup
Duplicate Combined Sources
Symptom: Multiple "MOTU M4 All Inputs (Mono)" sources appear.
Cause: Service was started multiple times without cleanup.
Fix:
# Stop and restart the service (cleanup happens automatically)
systemctl --user restart motu-m4-combined-setup
# Or manually clean up
pactl list modules short | grep -E "loopback.*motu_mixer|remap.*motu_m4_combined|null-sink.*motu_mixer" | cut -f1 | xargs -I {} pactl unload-module {}
Service Management
Restart the Setup
systemctl --user restart motu-m4-combined-setup
Check Service Status
systemctl --user status motu-m4-combined-setup
View Service Logs
journalctl --user -u motu-m4-combined-setup -f
Disable the Service (but keep it in config)
systemctl --user stop motu-m4-combined-setup
systemctl --user disable motu-m4-combined-setup
Manual Cleanup (if service fails)
# List all loaded modules
pactl list modules short
# Unload specific module by ID
pactl unload-module <MODULE_ID>
# Nuclear option: restart PipeWire (clears all modules)
systemctl --user restart pipewire-pulse
Testing & Verification
Verify Null Sink Created
pactl list sinks short | grep motu_mixer
# Expected output: <ID> motu_mixer PipeWire ...
Verify Loopbacks Created
pactl list modules short | grep loopback | grep motu_mixer
# Expected: 4 lines (one for each M4 input)
Verify Virtual Source Available
wpctl status | grep "MOTU M4 All Inputs"
# Expected: Line showing the virtual source in "Sources" section
Test Audio Flow
- Speak/play audio into M4 inputs
- Verify NO audio plays through your headphones/speakers (unless you explicitly route it)
- Open pavucontrol → Recording tab
- Select "MOTU M4 All Inputs (Mono)" as source
- Check that levels respond to your inputs
Design Decisions Log
2025-01-18: Initial Implementation
Decision: Create module with PipeWire declarative config for null sink + systemd service for loopbacks.
Rationale: Seemed like the "proper NixOS way" to use declarative PipeWire config.
Outcome: ❌ Null sink was never created. PipeWire's context.objects in drop-in configs doesn't work reliably.
2025-01-18: Move to Pure Systemd Service
Decision: Move null sink creation into systemd service, remove PipeWire declarative config entirely.
Rationale:
- NixOS PipeWire wiki explicitly states some tasks need scripts/services
- Guarantees proper ordering (sink before loopbacks)
- Self-contained and portable
- Easier to debug
Outcome: ✅ Works perfectly. No audio leakage, clean setup/teardown.
Idempotency & Cleanup
Decision: Add comprehensive ExecStop and ExecStopPost to clean up all modules.
Rationale:
- Service restarts should not create duplicate modules
- Failed starts should still allow cleanup
- Pattern-based matching (grep) is more reliable than tracking module IDs
Implementation:
ExecStop: Primary cleanup (loopbacks → remap → null sink, in that order)ExecStopPost: Failsafe cleanup using pattern matching
2025-01-12: Device-Conditional Service Activation
Decision: Bind systemd service lifecycle to MOTU M4 USB device presence.
Rationale:
- Service should only run when hardware is actually connected
- Prevents phantom virtual sources appearing when device is absent
- Automatic start/stop on device plug/unplug events
- Better user experience and resource management
Implementation:
- Udev rule: Tags MOTU M4 USB device (07fd:000b) for systemd tracking
- Service binding: Uses
bindsTo=to tie service lifecycle to device unit - ExecCondition: Pre-flight check verifies device is present AND PipeWire has enumerated audio sources
- Error handling: Removed
|| truefallbacks since device presence is now guaranteed
Related Resources
- NixOS PipeWire Wiki
- PipeWire Documentation
- PulseAudio Module Documentation
- WirePlumber Configuration
Future Improvements
Potential enhancements (not currently needed):
Auto-detection: Detect M4 connection/disconnection and auto-start/stop service✅ Implemented (2025-01-12)- Volume Control: Add per-input volume control before mixing
- Stereo Version: Create a stereo combined source option
- Multiple Devices: Support for other MOTU interfaces (M2, M6, etc.)
- GUI Configuration: NixOS module options exposed via Home Manager GUI
License
This module follows the NixOS project license (MIT).
Maintainer Notes
When updating this module:
-
Test both scenarios:
- M4 connected on boot
- M4 connected after boot
-
Verify cleanup:
- Restart service multiple times
- Check no duplicate modules accumulate
-
Check audio routing:
- Confirm NO audio plays through default sink
- Verify combined source works in applications
-
Update this documentation with any new findings or changes.