diff --git a/modules/hardware/definition.nix b/modules/hardware/definition.nix index aa4894565..5841cb028 100644 --- a/modules/hardware/definition.nix +++ b/modules/hardware/definition.nix @@ -141,5 +141,40 @@ in { SUBSYSTEM=="input",ATTRS{name}=="AT Translated Set 2 keyboard",GROUP="kvm" ''; }; + + audio = { + # With the current implementation, the whole PCI IOMMU group 14: + # 00:1f.x in the example from Lenovo X1 Carbon + # must be defined for passthrough to AudioVM + pciDevices = mkOption { + description = "PCI Devices to passthrough to AudioVM"; + type = types.listOf pciDevSubmodule; + default = []; + example = literalExpression '' + [ + { + path = "0000:00:1f.0"; + vendorId = "8086"; + productId = "519d"; + } + { + path = "0000:00:1f.3"; + vendorId = "8086"; + productId = "51ca"; + } + { + path = "0000:00:1f.4"; + vendorId = "8086"; + productId = "51a3"; + } + { + path = "0000:00:1f.5"; + vendorId = "8086"; + productId = "51a4"; + } + ] + ''; + }; + }; }; } diff --git a/modules/hardware/lenovo-x1/definitions/default.nix b/modules/hardware/lenovo-x1/definitions/default.nix index 9437accf6..06fc311a6 100644 --- a/modules/hardware/lenovo-x1/definitions/default.nix +++ b/modules/hardware/lenovo-x1/definitions/default.nix @@ -25,6 +25,7 @@ in { inherit (hwDefinition) disks; inherit (hwDefinition) network; inherit (hwDefinition) gpu; + inherit (hwDefinition) audio; virtioInputHostEvdevs = [ # Lenovo X1 touchpad and keyboard diff --git a/modules/hardware/lenovo-x1/definitions/x1-gen10.nix b/modules/hardware/lenovo-x1/definitions/x1-gen10.nix index 8cfc01b10..a4ccc39ff 100644 --- a/modules/hardware/lenovo-x1/definitions/x1-gen10.nix +++ b/modules/hardware/lenovo-x1/definitions/x1-gen10.nix @@ -29,4 +29,6 @@ productId = "46a6"; } ]; + + audio.pciDevices = []; } diff --git a/modules/hardware/lenovo-x1/definitions/x1-gen11.nix b/modules/hardware/lenovo-x1/definitions/x1-gen11.nix index 72818febd..2d3cd67b0 100644 --- a/modules/hardware/lenovo-x1/definitions/x1-gen11.nix +++ b/modules/hardware/lenovo-x1/definitions/x1-gen11.nix @@ -38,4 +38,34 @@ productId = "a7a1"; } ]; + + # With the current implementation, the whole PCI IOMMU group 14: + # 00:1f.x in the example from Lenovo X1 Carbon + # must be defined for passthrough to AudioVM + audio.pciDevices = [ + { + # ISA bridge: Intel Corporation Raptor Lake LPC/eSPI Controller (rev 01) + path = "0000:00:1f.0"; + vendorId = "8086"; + productId = "519d"; + } + { + # Audio device: Intel Corporation Raptor Lake-P/U/H cAVS (rev 01) + path = "0000:00:1f.3"; + vendorId = "8086"; + productId = "51ca"; + } + { + # SMBus: Intel Corporation Alder Lake PCH-P SMBus Host Controller (rev 01) + path = "0000:00:1f.4"; + vendorId = "8086"; + productId = "51a3"; + } + { + # Serial bus controller: Intel Corporation Alder Lake-P PCH SPI Controller (rev 01) + path = "0000:00:1f.5"; + vendorId = "8086"; + productId = "51a4"; + } + ]; } diff --git a/modules/microvm/default.nix b/modules/microvm/default.nix index 13c7f0fdc..2e7e12be9 100644 --- a/modules/microvm/default.nix +++ b/modules/microvm/default.nix @@ -12,6 +12,7 @@ ./virtualization/microvm/idsvm/mitmproxy ./virtualization/microvm/appvm.nix ./virtualization/microvm/guivm.nix + ./virtualization/microvm/audiovm.nix ./networking.nix ./power-control.nix ]; diff --git a/modules/microvm/virtualization/microvm/audiovm.nix b/modules/microvm/virtualization/microvm/audiovm.nix new file mode 100644 index 000000000..cd79e9fe3 --- /dev/null +++ b/modules/microvm/virtualization/microvm/audiovm.nix @@ -0,0 +1,116 @@ +# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + config, + lib, + ... +}: let + configHost = config; + vmName = "audio-vm"; + macAddress = "02:00:00:03:03:03"; + audiovmBaseConfiguration = { + imports = [ + (import ./common/vm-networking.nix {inherit vmName macAddress;}) + ({ + lib, + pkgs, + ... + }: { + ghaf = { + users.accounts.enable = lib.mkDefault configHost.ghaf.users.accounts.enable; + profiles.debug.enable = lib.mkDefault configHost.ghaf.profiles.debug.enable; + + development = { + ssh.daemon.enable = lib.mkDefault configHost.ghaf.development.ssh.daemon.enable; + debug.tools.enable = lib.mkDefault configHost.ghaf.development.debug.tools.enable; + nix-setup.enable = lib.mkDefault configHost.ghaf.development.nix-setup.enable; + }; + systemd = { + enable = true; + withName = "audiovm-systemd"; + withNss = true; + withResolved = true; + withTimesyncd = true; + withDebug = configHost.ghaf.profiles.debug.enable; + }; + }; + + environment = { + systemPackages = [ + pkgs.pulseaudio + pkgs.pamixer + pkgs.pipewire + ]; + }; + + system.stateVersion = lib.trivial.release; + + nixpkgs = { + buildPlatform.system = configHost.nixpkgs.buildPlatform.system; + hostPlatform.system = configHost.nixpkgs.hostPlatform.system; + }; + + microvm = { + optimize.enable = true; + vcpu = 1; + mem = 256; + hypervisor = "qemu"; + shares = [ + { + tag = "ro-store"; + source = "/nix/store"; + mountPoint = "/nix/.ro-store"; + } + ]; + writableStoreOverlay = lib.mkIf config.ghaf.development.debug.tools.enable "/nix/.rw-store"; + qemu = { + machine = + { + # Use the same machine type as the host + x86_64-linux = "q35"; + aarch64-linux = "virt"; + } + .${configHost.nixpkgs.hostPlatform.system}; + }; + }; + + imports = [ + ../../../common + ]; + + # Fixed IP-address for debugging subnet + systemd.network.networks."10-ethint0".addresses = [ + { + addressConfig.Address = "192.168.101.5/24"; + } + ]; + }) + ]; + }; + cfg = config.ghaf.virtualization.microvm.audiovm; +in { + options.ghaf.virtualization.microvm.audiovm = { + enable = lib.mkEnableOption "AudioVM"; + + extraModules = lib.mkOption { + description = '' + List of additional modules to be imported and evaluated as part of + AudioVM's NixOS configuration. + ''; + default = []; + }; + }; + + config = lib.mkIf cfg.enable { + microvm.vms."${vmName}" = { + autostart = true; + config = + audiovmBaseConfiguration + // { + imports = + audiovmBaseConfiguration.imports + ++ cfg.extraModules; + }; + }; + }; +} diff --git a/modules/microvm/virtualization/microvm/guivm.nix b/modules/microvm/virtualization/microvm/guivm.nix index d93c907d4..250fe6a9c 100644 --- a/modules/microvm/virtualization/microvm/guivm.nix +++ b/modules/microvm/virtualization/microvm/guivm.nix @@ -70,6 +70,7 @@ pkgs.waypipe pkgs.networkmanagerapplet pkgs.nm-launcher + pkgs.pamixer ] ++ (lib.optional (configHost.ghaf.profiles.debug.enable && configHost.ghaf.virtualization.microvm.idsvm.mitmproxy.enable) pkgs.mitmweb-ui); }; diff --git a/targets/lenovo-x1/appvms/chromium.nix b/targets/lenovo-x1/appvms/chromium.nix index 9cb7c250c..dbd3b4ca3 100644 --- a/targets/lenovo-x1/appvms/chromium.nix +++ b/targets/lenovo-x1/appvms/chromium.nix @@ -21,7 +21,7 @@ in { ''; in [ pkgs.chromium - pkgs.pamixer + pkgs.pulseaudio pkgs.xdg-utils xdgPdfItem xdgOpenPdf @@ -32,22 +32,26 @@ in { cores = 4; extraModules = [ { - # Enable pulseaudio for user ghaf + # Enable pulseaudio for Chromium VM + security.rtkit.enable = true; sound.enable = true; - hardware.pulseaudio.enable = true; - users.extraUsers.ghaf.extraGroups = ["audio"]; + users.extraUsers.ghaf.extraGroups = ["audio" "video"]; + + hardware.pulseaudio = { + enable = true; + extraConfig = '' + load-module module-tunnel-sink sink_name=chromium-speaker server=audio-vm.ghaf:4713 format=s16le channels=2 rate=48000 + load-module module-tunnel-source source_name=chromium-mic server=audio-vm.ghaf:4713 format=s16le channels=1 rate=48000 + + # Set sink and source default max volume to about 90% (0-65536) + set-sink-volume chromium-speaker 60000 + set-source-volume chromium-mic 60000 + ''; + }; time.timeZone = "Asia/Dubai"; microvm.qemu.extraArgs = [ - # Connect sound device to hosts pulseaudio socket - "-audiodev" - "pa,id=pa1,server=unix:/run/pulse/native" - # Add HDA sound device to guest - "-device" - "intel-hda" - "-device" - "hda-duplex,audiodev=pa1" # Lenovo X1 integrated usb webcam "-device" "qemu-xhci" diff --git a/targets/lenovo-x1/appvms/element.nix b/targets/lenovo-x1/appvms/element.nix index 3423ec0e9..7a3e01861 100644 --- a/targets/lenovo-x1/appvms/element.nix +++ b/targets/lenovo-x1/appvms/element.nix @@ -26,10 +26,22 @@ in { extraModules = [ { # Enable pulseaudio for user ghaf to access mic + security.rtkit.enable = true; sound.enable = true; - hardware.pulseaudio.enable = true; users.extraUsers.ghaf.extraGroups = ["audio" "video"]; + hardware.pulseaudio = { + enable = true; + extraConfig = '' + load-module module-tunnel-sink sink_name=element-speaker server=audio-vm.ghaf:4713 format=s16le channels=2 rate=48000 + load-module module-tunnel-source source_name=element-mic server=audio-vm.ghaf:4713 format=s16le channels=1 rate=48000 + + # Set sink and source default max volume to about 90% (0-65536) + set-sink-volume element-speaker 60000 + set-source-volume element-mic 60000 + ''; + }; + systemd = { services = { element-gps = { @@ -98,14 +110,6 @@ in { "qemu-xhci" "-device" "usb-host,hostbus=3,hostport=8" - # Connect sound device to hosts pulseaudio socket - "-audiodev" - "pa,id=pa1,server=unix:/run/pulse/native" - # Add HDA sound device to guest - "-device" - "intel-hda" - "-device" - "hda-duplex,audiodev=pa1" # External USB GPS receiver "-device" "usb-host,vendorid=0x067b,productid=0x23a3" diff --git a/targets/lenovo-x1/audiovmExtraModules.nix b/targets/lenovo-x1/audiovmExtraModules.nix new file mode 100644 index 000000000..d1ff047b0 --- /dev/null +++ b/targets/lenovo-x1/audiovmExtraModules.nix @@ -0,0 +1,99 @@ +# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +# +{ + lib, + pkgs, + microvm, + configH, + ... +}: let + # TCP port used by Pipewire-pulseaudio service + pulseaudioTcpPort = 4713; + + audiovmPCIPassthroughModule = { + microvm.devices = lib.mkForce ( + builtins.map (d: { + bus = "pci"; + inherit (d) path; + }) + configH.ghaf.hardware.definition.audio.pciDevices + ); + }; + + audiovmExtraConfigurations = { + time.timeZone = "Asia/Dubai"; + + # Enable pipewire service for audioVM with pulseaudio support + security.rtkit.enable = true; + sound.enable = true; + hardware.firmware = [pkgs.sof-firmware]; + services.pipewire = { + enable = true; + pulse.enable = true; + systemWide = true; + + configPackages = [ + (pkgs.writeTextDir "share/pipewire/pipewire.conf.d/10-remote-simple.conf" '' + context.modules = [ + { name = libpipewire-module-protocol-pulse + args = { + server.address = [ + "tcp:4713" # IPv4 and IPv6 on all addresses + ]; + pulse.min.req = 128/48000; # 2.7ms + pulse.default.req = 960/48000; # 20 milliseconds + pulse.min.frag = 128/48000; # 2.7ms + pulse.default.frag = 512/48000; # ~10 ms + pulse.default.tlength = 512/48000; # ~10 ms + pulse.min.quantum = 128/48000; # 2.7ms + } + } + ]; + '') + ]; + }; + + hardware.pulseaudio.extraConfig = '' + # Set sink and source default max volume to about 75% (0-65536) + set-sink-volume @DEFAULT_SINK@ 48000 + set-source-volume @DEFAULT_SOURCE@ 48000 + ''; + + # Allow ghaf user to access pulseaudio and pipewire + users.extraUsers.ghaf.extraGroups = ["audio" "video" "pulse-access" "pipewire"]; + + # Dummy service to get pipewire and pulseaudio services started at boot + # Normally Pipewire and pulseaudio are started when they are needed by user, + # We don't have users in audiovm so we need to give PW/PA a slight kick.. + # This calls pulseaudios pa-info binary to get information about pulseaudio current + # state which starts pipewire-pulseaudio service in the process. + systemd.services.pulseaudio-starter = { + after = ["pipewire.service" "network-online.target"]; + requires = ["pipewire.service" "network-online.target"]; + wantedBy = ["default.target"]; + path = [pkgs.coreutils]; + enable = true; + serviceConfig = { + User = "ghaf"; + Group = "ghaf"; + }; + script = ''${pkgs.pulseaudio}/bin/pa-info > /dev/null 2>&1''; + }; + + # Open TCP port for the PDF XDG socket + networking.firewall.allowedTCPPorts = [pulseaudioTcpPort]; + + microvm = { + qemu.extraArgs = [ + ]; + kernelParams = [ + "snd_intel_dspcfg.dsp_driver=3" + "snd_sof_intel_hda_common.dmic_num=4" + ]; + }; + }; +in [ + audiovmPCIPassthroughModule + audiovmExtraConfigurations +] diff --git a/targets/lenovo-x1/everything.nix b/targets/lenovo-x1/everything.nix index d444b1b03..ae99fbeea 100644 --- a/targets/lenovo-x1/everything.nix +++ b/targets/lenovo-x1/everything.nix @@ -36,6 +36,7 @@ vfioPciIds = mapPciIdsToString (filterDevices ( config.ghaf.hardware.definition.network.pciDevices ++ config.ghaf.hardware.definition.gpu.pciDevices + ++ config.ghaf.hardware.definition.audio.pciDevices )); in { boot = { @@ -52,19 +53,25 @@ initrd.availableKernelModules = ["nvme"]; }; - security.polkit.extraConfig = powerControl.polkitExtraConfig; - time.timeZone = "Asia/Dubai"; - - # Enable pulseaudio support for host as a service - sound.enable = true; - hardware.pulseaudio = { + security.polkit = { enable = true; - systemWide = true; - # Allow microvm user to access pulseaudio - extraConfig = "load-module module-combine-sink module-native-protocol-unix auth-anonymous=1"; + extraConfig = powerControl.polkitExtraConfig; }; + time.timeZone = "Asia/Dubai"; - users.extraUsers.microvm.extraGroups = ["audio" "pulse-access"]; + systemd.services."microvm@audio-vm".serviceConfig = { + # The + here is a systemd feature to make the script run as root. + ExecStopPost = [ + "+${pkgs.writeShellScript "reload-audio" '' + # The script makes audio device internal state to reset + # This fixes issue of audio device getting into some unexpected + # state when the VM is being shutdown during audio mic recording + echo "1" > /sys/bus/pci/devices/0000:00:1f.3/remove + sleep 0.1 + echo "1" > /sys/bus/pci/devices/0000:00:1f.0/rescan + ''}" + ]; + }; disko.devices.disk = config.ghaf.hardware.definition.disks; @@ -118,6 +125,14 @@ }; }; + audiovm = { + enable = true; + extraModules = import ./audiovmExtraModules.nix { + inherit lib pkgs microvm; + configH = config; + }; + }; + appvm = { enable = true; vms = import ./appvms/default.nix {inherit pkgs lib config;}; diff --git a/targets/lenovo-x1/guivmExtraModules.nix b/targets/lenovo-x1/guivmExtraModules.nix index 809968818..4d394dc00 100644 --- a/targets/lenovo-x1/guivmExtraModules.nix +++ b/targets/lenovo-x1/guivmExtraModules.nix @@ -184,9 +184,6 @@ # Lenovo X1 AC adapter "-device" "acad" - # Connect sound device to hosts pulseaudio socket - "-audiodev" - "pa,id=pa1,server=unix:/run/pulse/native" ] ++ lib.optionals configH.ghaf.hardware.fprint.enable configH.ghaf.hardware.fprint.qemuExtraArgs; };