# 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 <MODULE_ID>

# 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: <ID>  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.
