diff --git a/modules/common/services/audio.nix b/modules/common/services/audio.nix index a7b198845..d953840e3 100644 --- a/modules/common/services/audio.nix +++ b/modules/common/services/audio.nix @@ -8,7 +8,7 @@ ... }: let cfg = config.ghaf.services.audio; - inherit (lib) mkIf mkEnableOption mkOption types; + inherit (lib) mkIf mkEnableOption mkOption literalExpression types; in { options.ghaf.services.audio = { enable = mkEnableOption "Enable audio service for audio VM"; @@ -17,6 +17,17 @@ in { default = 4713; description = "TCP port used by Pipewire-pulseaudio service"; }; + appStreams = mkOption { + description = "Audio streams for ghaf applications"; + type = types.listOf types.str; + default = []; + example = literalExpression '' + [ + "chromium" + "element" + ] + ''; + }; }; config = mkIf cfg.enable { @@ -35,19 +46,63 @@ in { { name = libpipewire-module-protocol-pulse args = { server.address = [ - "tcp:4713" # IPv4 and IPv6 on all addresses + { + # Listen TCP port (4713 by default) for IPv4 and IPv6 on all addresses + address = "tcp:0.0.0.0:${toString cfg.pulseaudioTcpPort}" + max-clients = 64 + listen-backlog = 32 + client.access = "unrestricted" + } ]; - 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 + + pulse = { + min.req = 128/48000; # 2.7ms + default.req = 960/48000; # 20 milliseconds + default.tlength = 4800/48000; # 100 ms + # recording buffer options + min.frag = 128/48000; # 2.7ms + default.frag = 960/48000; # 20 ms + # Sheduling + min.quantum = 128/48000; # 2.7ms (test with 1024) + } } } ]; '') ]; + + extraConfig.pipewire = builtins.listToAttrs ( + map (name: + lib.attrsets.nameValuePair "91-chennels-${name}-vm" + { + "context.objects" = [ + { + factory = "adapter"; + args = { + "factory.name" = "support.null-audio-sink"; + "node.name" = "${name}.mic"; + "node.description" = "${name} Microphone"; + "media.class" = "Audio/Source/Virtual"; + "audio.position" = "MONO"; + "target.object" = "@DEFAULT_SOURCE@"; + }; + } + { + factory = "adapter"; + args = { + "factory.name" = "support.null-audio-sink"; + "node.name" = "${name}.speaker"; + "node.description" = "${name} Speaker"; + "media.class" = "Audio/Sink"; + "audio.position" = "FL,FR"; + "target.object" = "@DEFAULT_SINK@"; + }; + } + ]; + } + ) + cfg.appStreams + ); }; hardware.pulseaudio.extraConfig = '' @@ -77,6 +132,51 @@ in { script = ''${pkgs.pulseaudio}/bin/pa-info > /dev/null 2>&1''; }; + # TODO Automate and fix alsa IO ports + systemd.services."pipewire-link-starter" = let + + audioLinks = lib.strings.concatLines ( + lib.lists.forEach cfg.appStreams (name: + '' + ${pkgs.pipewire}/bin/pw-link --passive $ALSA_PCI_MIC_R ${name}.mic:input_MONO + ${pkgs.pipewire}/bin/pw-link --passive ${name}.speaker:monitor_FR $ALSA_PCI_SPK_R + ${pkgs.pipewire}/bin/pw-link --passive ${name}.speaker:monitor_FL $ALSA_PCI_SPK_L + '' + ) + ); + + # Read first alsa IO ports from pipewire link output and connect to those + pipewireLinkStarterScript = pkgs.writeShellScriptBin "pipewire-link-starter" '' + ALSA_PCI_MIC_R=$(${pkgs.pipewire}/bin/pw-link --output | ${pkgs.gawk}/bin/awk '/alsa_input.pci/ && /capture_FR/{print $1;exit}') + ALSA_PCI_SPK_R=$(${pkgs.pipewire}/bin/pw-link --input | ${pkgs.gawk}/bin/awk '/alsa_output.pci/ && /playback_FR/{print $1;exit}') + ALSA_PCI_SPK_L=$(${pkgs.pipewire}/bin/pw-link --input | ${pkgs.gawk}/bin/awk '/alsa_output.pci/ && /playback_FL/{print $1;exit}') + + if [ -z "''${ALSA_PCI_MIC_R}" ] || [ -z "''${ALSA_PCI_SPK_R}" ] || [ -z "''${ALSA_PCI_SPK_L}" ]; then + echo "No Pipewire-Alsa devices available." + exit 1 + fi + + ${audioLinks} + exit 0 + ''; + in { + enable = true; + description = "Connect pipewire VM audio links"; + path = [pipewireLinkStarterScript]; + wantedBy = ["default.target"]; + after = ["pipewire.service" "network-online.target" "pulseaudio-starter.service"]; + requires = ["pipewire.service" "network-online.target" "pulseaudio-starter.service"]; + serviceConfig = { + Type = "simple"; + StandardOutput = "journal"; + StandardError = "journal"; + ExecStart = "${pipewireLinkStarterScript}/bin/pipewire-link-starter"; + Restart = "on-failure"; + RestartSec = "5"; + StartLimitBurst = "5"; + }; + }; + # Open TCP port for the PDF XDG socket networking.firewall.allowedTCPPorts = [cfg.pulseaudioTcpPort]; }; diff --git a/modules/microvm/virtualization/microvm/appvm.nix b/modules/microvm/virtualization/microvm/appvm.nix index ac46d31ff..7c95bb632 100644 --- a/modules/microvm/virtualization/microvm/appvm.nix +++ b/modules/microvm/virtualization/microvm/appvm.nix @@ -92,6 +92,28 @@ pkgs.opensc ]; + # Enable Ghaf audio for Appvm + # TODO maybe make a separate module that can be loaded as per $vm.ghafAudio + +# config.ghaf.virtualization.microvm.audiovm.appStreams = +# lib.optional (vm.ghafAudio) "${vm.name}"; + + sound.enable = vm.ghafAudio; + security.rtkit.enable = vm.ghafAudio; + users.extraUsers.ghaf.extraGroups = + lib.optionals vm.ghafAudio ["audio" "video"]; + hardware.pulseaudio.enable = vm.ghafAudio; + hardware.pulseaudio.extraConfig = '' + ${lib.optionalString vm.ghafAudio '' + load-module module-tunnel-sink sink=${vm.name}.speaker sink_name=${vm.name}.speaker server=audio-vm:4713 format=s16le channels=2 rate=48000 + load-module module-tunnel-source source=${vm.name}.mic source_name=${vm.name}.mic server=audio-vm:4713 format=s16le channels=1 rate=48000 + + # Set sink and source default max volume to about 90% (0-65536) + set-sink-volume ${vm.name}.speaker 60000 + set-source-volume ${vm.name}.mic 60000 + ''} + ''; + security.tpm2 = { enable = true; abrmd.enable = true; @@ -220,6 +242,11 @@ in { type = types.nullOr types.str; default = null; }; + ghafAudio = mkOption { + description = "Enable Ghaf VM Audio support"; + type = types.bool; + default = false; + }; vtpm.enable = lib.mkEnableOption "vTPM support in the virtual machine"; }; }); @@ -234,6 +261,12 @@ in { default = []; }; + ghafAudio = mkOption { + description = "Enable Ghaf VM Audio support"; + type = types.bool; + default = false; + }; + # Base VSOCK CID which is used for auto assigning CIDs for all AppVMs # For example, when it's set to 100, AppVMs will get 100, 101, 102, etc. # It is also possible to override the auto assinged CID using the vms.cid option diff --git a/modules/microvm/virtualization/microvm/audiovm.nix b/modules/microvm/virtualization/microvm/audiovm.nix index 6813393bb..876410736 100644 --- a/modules/microvm/virtualization/microvm/audiovm.nix +++ b/modules/microvm/virtualization/microvm/audiovm.nix @@ -6,6 +6,8 @@ pkgs, ... }: let + inherit (lib) mkOption literalExpression types optionals; + configHost = config; vmName = "audio-vm"; macAddress = "02:00:00:03:03:03"; @@ -48,7 +50,11 @@ withTimesyncd = true; withDebug = configHost.ghaf.profiles.debug.enable; }; - services.audio.enable = true; + services.audio = { + enable = true; + # TODO Get list of appstreams from global cfg + appStreams = config.ghaf.virtualization.microvm.audiovm.appStreams; + }; }; environment = { @@ -56,6 +62,7 @@ pkgs.pulseaudio pkgs.pamixer pkgs.pipewire + pkgs.pw-volume ]; }; @@ -127,6 +134,18 @@ in { options.ghaf.virtualization.microvm.audiovm = { enable = lib.mkEnableOption "AudioVM"; + appStreams = lib.mkOption { + description = "Audio streams for ghaf applications"; + type = types.listOf types.str; + default = [ "chromium" "element" "business" ]; + example = literalExpression '' + [ + "chromium" + "element" + ] + ''; + }; + extraModules = lib.mkOption { description = '' List of additional modules to be imported and evaluated as part of diff --git a/modules/reference/appvms/business.nix b/modules/reference/appvms/business.nix index 8cef4fcd1..fea349755 100644 --- a/modules/reference/appvms/business.nix +++ b/modules/reference/appvms/business.nix @@ -27,7 +27,6 @@ in { ''; in [ pkgs.chromium - pkgs.pulseaudio pkgs.xdg-utils xdgPdfItem xdgOpenPdf @@ -42,23 +41,6 @@ in { extraModules = [ { imports = [../programs/chromium.nix]; - # Enable pulseaudio for Chromium VM - security.rtkit.enable = true; - sound.enable = true; - users.extraUsers.ghaf.extraGroups = ["audio" "video"]; - - hardware.pulseaudio = { - enable = true; - extraConfig = '' - load-module module-tunnel-sink sink_name=chromium-speaker server=audio-vm:4713 format=s16le channels=2 rate=48000 - load-module module-tunnel-source source_name=chromium-mic server=audio-vm: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 = config.time.timeZone; microvm = { @@ -246,5 +228,6 @@ in { } ]; borderColor = "#00FF00"; + ghafAudio = true; vtpm.enable = true; } diff --git a/modules/reference/appvms/chromium.nix b/modules/reference/appvms/chromium.nix index 0c0af8a5e..38ccc7574 100644 --- a/modules/reference/appvms/chromium.nix +++ b/modules/reference/appvms/chromium.nix @@ -27,7 +27,6 @@ in { ''; in [ pkgs.chromium - pkgs.pulseaudio pkgs.xdg-utils xdgPdfItem xdgOpenPdf @@ -38,23 +37,9 @@ in { cores = 4; extraModules = [ { - imports = [../programs/chromium.nix]; - # Enable pulseaudio for Chromium VM - security.rtkit.enable = true; - sound.enable = true; - users.extraUsers.ghaf.extraGroups = ["audio" "video"]; - - hardware.pulseaudio = { - enable = true; - extraConfig = '' - load-module module-tunnel-sink sink_name=chromium-speaker server=audio-vm:4713 format=s16le channels=2 rate=48000 - load-module module-tunnel-source source_name=chromium-mic server=audio-vm: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 - ''; - }; + imports = [ + ../programs/chromium.nix + ]; time.timeZone = config.time.timeZone; @@ -70,5 +55,6 @@ in { } ]; borderColor = "#630505"; + ghafAudio = true; vtpm.enable = true; } diff --git a/modules/reference/appvms/element.nix b/modules/reference/appvms/element.nix index 0eb06f288..d24612de5 100644 --- a/modules/reference/appvms/element.nix +++ b/modules/reference/appvms/element.nix @@ -30,23 +30,6 @@ in { cores = 4; extraModules = [ { - # Enable pulseaudio for user ghaf to access mic - security.rtkit.enable = true; - sound.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:4713 format=s16le channels=2 rate=48000 - load-module module-tunnel-source source_name=element-mic server=audio-vm: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 = { @@ -97,4 +80,5 @@ in { } ]; borderColor = "#337aff"; + ghafAudio = true; } diff --git a/modules/reference/services/vm-audio.nix b/modules/reference/services/vm-audio.nix new file mode 100644 index 000000000..2a6797f08 --- /dev/null +++ b/modules/reference/services/vm-audio.nix @@ -0,0 +1,33 @@ +# Copyright 2022-2024 TII (SSRC) and the Ghaf contributors +# SPDX-License-Identifier: Apache-2.0 +{ + pkgs, + config, + vmName, + ... +}: let + speakerName = "${vmName}.speaker"; + micName = "${vmName}.mic"; +in { +# packages = [ +# pkgs.pulseaudio +# pkgs.pamixer +# ]; + + # Enable pulseaudio for application VM + security.rtkit.enable = true; + sound.enable = true; + users.extraUsers.ghaf.extraGroups = ["audio" "video"]; + + hardware.pulseaudio = { + enable = true; + extraConfig = " + load-module module-tunnel-sink sink=${speakerName} sink_name=${speakerName} server=audio-vm:4713 format=s16le channels=2 rate=48000 + load-module module-tunnel-source source=${micName} source_name=${micName} server=audio-vm:4713 format=s16le channels=1 rate=48000 + + # Set sink and source default max volume to about 90% (0-65536) + set-sink-volume ${speakerName} 60000 + set-source-volume ${micName} 60000 + "; + }; +}