From 58f9b354b01d17428b5a74a3bfa492f60c960191 Mon Sep 17 00:00:00 2001 From: Jon Sahlberg Date: Tue, 9 Apr 2024 12:19:51 +0300 Subject: [PATCH] Initial AudioVM implementation with pipewire and pulseaudio Initial version of AudioVM with Pipewire backend and pulseaudio TCP remote communication layer for the guest VMs. Note that this is not really secure design (yet) basically all VMs can access the pulseaudio TCP service. Signed-off-by: Jon Sahlberg --- modules/hardware/definition.nix | 35 ++++++ .../lenovo-x1/definitions/default.nix | 1 + .../lenovo-x1/definitions/x1-gen10.nix | 2 + .../lenovo-x1/definitions/x1-gen11.nix | 30 +++++ modules/microvm/default.nix | 1 + .../virtualization/microvm/audiovm.nix | 116 ++++++++++++++++++ .../microvm/virtualization/microvm/guivm.nix | 1 + targets/lenovo-x1/appvms/chromium.nix | 28 +++-- targets/lenovo-x1/appvms/element.nix | 22 ++-- targets/lenovo-x1/audiovmExtraModules.nix | 99 +++++++++++++++ targets/lenovo-x1/everything.nix | 35 ++++-- targets/lenovo-x1/guivmExtraModules.nix | 3 - 12 files changed, 339 insertions(+), 34 deletions(-) create mode 100644 modules/microvm/virtualization/microvm/audiovm.nix create mode 100644 targets/lenovo-x1/audiovmExtraModules.nix 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; };