# 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 1. **Null Sink (`motu_mixer`)**: Internal PulseAudio/PipeWire sink that receives audio from all inputs 2. **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) 3. **Remap Source Module**: Exposes `motu_mixer.monitor` as 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: 1. **Udev tagging**: USB device is tagged for systemd tracking 2. **Device binding**: Service uses `bindsTo=` to tie its lifecycle to the USB device 3. **ExecCondition**: Additional validation ensures PipeWire has enumerated the audio sources before setup ## Configuration Options ```nix 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**: 1. **PipeWire config limitations**: The `services.pipewire.extraConfig.pipewire` only works for property-based configuration. It cannot reliably create `context.objects` (null sinks) or load PulseAudio compatibility modules. 2. **Portability**: Self-contained service works on any NixOS system with PipeWire, no special PipeWire configuration needed. 3. **Guaranteed ordering**: Null sink is created before loopbacks try to connect to it. 4. **Easier debugging**: Everything in one service, one log stream to check. **Reference**: [NixOS PipeWire Wiki - Null Sinks](https://wiki.nixos.org/wiki/PipeWire#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**: ```bash # 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**: 1. M4 is not connected (most common - service won't start if device is absent) 2. Service condition check failed (device present but audio sources not ready) 3. Service failed to start 4. PipeWire/WirePlumber not running **Fix**: ```bash # 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**: ```bash # 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 ```bash systemctl --user restart motu-m4-combined-setup ``` ### Check Service Status ```bash systemctl --user status motu-m4-combined-setup ``` ### View Service Logs ```bash journalctl --user -u motu-m4-combined-setup -f ``` ### Disable the Service (but keep it in config) ```bash systemctl --user stop motu-m4-combined-setup systemctl --user disable motu-m4-combined-setup ``` ### Manual Cleanup (if service fails) ```bash # List all loaded modules pactl list modules short # Unload specific module by ID pactl unload-module # Nuclear option: restart PipeWire (clears all modules) systemctl --user restart pipewire-pulse ``` ## Testing & Verification ### Verify Null Sink Created ```bash pactl list sinks short | grep motu_mixer # Expected output: motu_mixer PipeWire ... ``` ### Verify Loopbacks Created ```bash pactl list modules short | grep loopback | grep motu_mixer # Expected: 4 lines (one for each M4 input) ``` ### Verify Virtual Source Available ```bash wpctl status | grep "MOTU M4 All Inputs" # Expected: Line showing the virtual source in "Sources" section ``` ### Test Audio Flow 1. Speak/play audio into M4 inputs 2. Verify NO audio plays through your headphones/speakers (unless you explicitly route it) 3. Open pavucontrol → Recording tab 4. Select "MOTU M4 All Inputs (Mono)" as source 5. 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**: 1. **Udev rule**: Tags MOTU M4 USB device (07fd:000b) for systemd tracking 2. **Service binding**: Uses `bindsTo=` to tie service lifecycle to device unit 3. **ExecCondition**: Pre-flight check verifies device is present AND PipeWire has enumerated audio sources 4. **Error handling**: Removed `|| true` fallbacks since device presence is now guaranteed ## Related Resources - [NixOS PipeWire Wiki](https://wiki.nixos.org/wiki/PipeWire) - [PipeWire Documentation](https://docs.pipewire.org/) - [PulseAudio Module Documentation](https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/User/Modules/) - [WirePlumber Configuration](https://pipewire.pages.freedesktop.org/wireplumber/) ## Future Improvements Potential enhancements (not currently needed): 1. ~~**Auto-detection**: Detect M4 connection/disconnection and auto-start/stop service~~ ✅ Implemented (2025-01-12) 2. **Volume Control**: Add per-input volume control before mixing 3. **Stereo Version**: Create a stereo combined source option 4. **Multiple Devices**: Support for other MOTU interfaces (M2, M6, etc.) 5. **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: 1. **Test both scenarios**: - M4 connected on boot - M4 connected after boot 2. **Verify cleanup**: - Restart service multiple times - Check no duplicate modules accumulate 3. **Check audio routing**: - Confirm NO audio plays through default sink - Verify combined source works in applications 4. **Update this documentation** with any new findings or changes.