From 5230da7b0c0ed8a31a74f2a82729da458dbe669a Mon Sep 17 00:00:00 2001 From: Nate Anderson Date: Fri, 12 Dec 2025 10:58:40 -0700 Subject: [PATCH] fix motu source, add gcc --- flake.lock | 12 +- nate-work/dotfiles/kanshi/config | 11 +- nate-work/modules/home-manager/home.nix | 3 +- shared/modules/services/motu-m4-combined.md | 301 +++++++++++++++++++ shared/modules/services/motu-m4-combined.nix | 59 +++- 5 files changed, 367 insertions(+), 19 deletions(-) create mode 100644 shared/modules/services/motu-m4-combined.md diff --git a/flake.lock b/flake.lock index 6742246..e1d1cb5 100644 --- a/flake.lock +++ b/flake.lock @@ -94,11 +94,11 @@ }, "nixpkgs-unstable": { "locked": { - "lastModified": 1764667669, - "narHash": "sha256-7WUCZfmqLAssbDqwg9cUDAXrSoXN79eEEq17qhTNM/Y=", + "lastModified": 1765186076, + "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=", "owner": "nixos", "repo": "nixpkgs", - "rev": "418468ac9527e799809c900eda37cbff999199b6", + "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8", "type": "github" }, "original": { @@ -110,11 +110,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1764831616, - "narHash": "sha256-OtzF5wBvO0jgW1WW1rQU9cMGx7zuvkF7CAVJ1ypzkxA=", + "lastModified": 1765311797, + "narHash": "sha256-mSD5Ob7a+T2RNjvPvOA1dkJHGVrNVl8ZOrAwBjKBDQo=", "owner": "nixos", "repo": "nixpkgs", - "rev": "c97c47f2bac4fa59e2cbdeba289686ae615f8ed4", + "rev": "09eb77e94fa25202af8f3e81ddc7353d9970ac1b", "type": "github" }, "original": { diff --git a/nate-work/dotfiles/kanshi/config b/nate-work/dotfiles/kanshi/config index a19e723..8e94bf7 100644 --- a/nate-work/dotfiles/kanshi/config +++ b/nate-work/dotfiles/kanshi/config @@ -12,14 +12,15 @@ profile lg-ultragear { output eDP-1 disable } +# Profile: Work Ultrawide +profile work-ultrawide { + output "LG Electronics LG HDR WQHD 404MXNU37415" mode 3440x1440@99.98Hz position 0,0 scale 1.00 adaptive_sync on + output eDP-1 disable +} + # Profile: Laptop + Ultrawide (eDP-2) profile ultrawide-2 { output eDP-2 mode 3440x1440@99.98Hz position 0,0 scale 1.00 adaptive_sync on output eDP-1 disable } -# Profile: Laptop + Ultrawide (eDP-3) -profile ultrawide-3 { - output eDP-3 mode 3440x1440@99.98Hz position 0,0 scale 1.00 adaptive_sync on - output eDP-1 disable -} diff --git a/nate-work/modules/home-manager/home.nix b/nate-work/modules/home-manager/home.nix index a37062b..941f879 100644 --- a/nate-work/modules/home-manager/home.nix +++ b/nate-work/modules/home-manager/home.nix @@ -49,13 +49,14 @@ # helix vscode-fhs - unstable.distrobox unstable.docker_25 docker-compose jq gnumake mariadb cmake + gcc + oxker # docker desktop tui <3 ## nodejs frontend nodejs_24 husky diff --git a/shared/modules/services/motu-m4-combined.md b/shared/modules/services/motu-m4-combined.md new file mode 100644 index 0000000..ffd0ad7 --- /dev/null +++ b/shared/modules/services/motu-m4-combined.md @@ -0,0 +1,301 @@ +# 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. diff --git a/shared/modules/services/motu-m4-combined.nix b/shared/modules/services/motu-m4-combined.nix index 5c13e86..af4577e 100644 --- a/shared/modules/services/motu-m4-combined.nix +++ b/shared/modules/services/motu-m4-combined.nix @@ -35,20 +35,64 @@ in 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"; - after = [ "pipewire-pulse.service" ]; + # 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 = [ "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 @@ -60,26 +104,27 @@ in channels=1 \ channel_map=mono - # Create loopbacks from each M4 input to the mixer (fail gracefully if M4 not connected) + # 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 || true + 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 || true + 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 || true + 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 || true + 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 \