fix motu source, add gcc
This commit is contained in:
parent
429b8ee99c
commit
5230da7b0c
12
flake.lock
generated
12
flake.lock
generated
@ -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": {
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
301
shared/modules/services/motu-m4-combined.md
Normal file
301
shared/modules/services/motu-m4-combined.md
Normal file
@ -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 <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.
|
||||
@ -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 \
|
||||
|
||||
Loading…
Reference in New Issue
Block a user