Pipewire's extraConfig & wireplumber's extraConfig virtual microphone/source confusion?

hello everyone! :frog:

i have decided to micspam with pipewire, that is literally playing sounds/music into the virtual microphone via software only, so, hardware-less, oh, you know, funny haha “soundboards” and all, on nixos, of all things. am i crazy? yes.

so anyway, bit of context, i’ve been using this neat little tool:

and it’s been fantastic (the part where you can create a virtual input/combined/output device). BUT!

what is not fantastic is that streams or clients (of "media.class") (such as apps, that are playing back audio, like a browser or a media player, e.g. "~alsa_playback.*") automatically disconnect or suspend the links between the nodes after 3-5 seconds (or IMMEDIATELY!) when they are either:

A) “idling”, muting or skipping (seeking to a different time position) (e.g. tab or youtube html5 player), or playing NO sound at 0 volume, this does NOT include fadeouts in-between tracks in a continuous album as one file

B) browser tab reloading (from going to the next track or simply PAUSING the track) and/or music player (e.g. mpv, vlc, moc, ft2 (i know its not a “player”, its a tracker, but im just listing things), etc.) ending its track/playback

with A, i understand how this works: keep. playing. constant. audio. FOREVER. or else you will lose connection (the link). that’s funny! …that you have to reconnect the source to the microphone EVERY TIME for whatever reason when it’s not playing audio. i do it via pkgs.helvum, btw.

with B, i have no idea why this is a thing, but sometimes the apps (sources, but actually clients, i believe? aka internally alsa_playback) change their node AND serial id’s - that is NOT funny. HONESTLY! i was pretty frustrated with this and i honestly think that micro$oft winblows did it better, even with their shitty latency. it was just easier (VAC and/or voicemeeter). on linux, they disappear from the pipewire’s session forever??? wow thank you. sorry. fak corpos tho.

ANYWAY!

the so-called “node suspension” is the problem, right? no? NO! it didnt work! nothing down below worked. now this right here is hilarious:

  services.pipewire.wireplumber.extraConfig."99-disable-suspend" = {
    "monitor.alsa.rules" = [
      {
        matches = [
          {
            "node.name" = "~alsa_input.*"; # sources
          }
          {
            "node.name" = "~alsa_output.*"; # sinks
          }
          {
            "node.name" = "~alsa_playback.*"; # apps/clients -- EDIT: WRONG! this is supposed to be in "services.pipewire.extraConfig.client" option!
          }
          # ...
        ];
        actions = {
          update-props = { # NONE of this works btw
            "session.suspend-timeout-seconds" = 0; # 'systemctl --user restart wireplumber'!
            "node.pause-on-idle" = false;
            "node.suspend-on-idle" = false;
            "node.always-process" = true;
            "dither.method" = "wannamaker3"; # OPTIONAL shape -- rectangular, triangular, triangular-hf, wannamaker3, shaped5
            "dither.noise" = 2; # integer -- doesnt work???
            "object.linger" = true; # keep linked even if destroyed -- DOESNT WORK BTW
            # ...
          };
        };
      }
    ];
  };

EDIT: as i later found out, this is the only WIREPLUMBER config that works - for inputs/outputs/sources/sinks, but not for clients/apps/playback, apparently, where no properties are added at all. for PIPEWIRE, there is extraConfig.client, which i will talk about later on, sorry for the confusion, but despite it actually applying properties, SOME of them have no effect whatsoever, and it is driving me nuts. ALSO, i appended this snippet to EVERY single other pw/wp config section, just in case i “missed” some “incompatible” properties.

…which is taken and improved on from

SO! none of the above works btw. the update-props (pipewire wiki) thingie. OH! does this file even exist? that’s a good question! gonna check it in real time… it sure does! multiples of it in the nix store. not a write issue.

syntax issue? looks evaluable to me… hmm, would you look at that! even updating works! that is incredible. i wonder what else could it be…?!

aaaand once again, "session.suspend-timeout-seconds" and "node.always-process" (and others) are the things that i need for nodes to, ya know, never disconnect or be deleted on “idle” or whatever. one option is more underdocumented than the other. then i need "dither.noise" to produce a constant background noise for the nodes to… again, not disconnect, haha! going insane! i EVEN tried cranking up the noise to 10, 50, 100 (very loud), but i heard nothing. it’s just nothing. THAT is also weird.

speaking of noise, only from the pw-dump cli do i get more information about it, instead of the docs. epic.

          {
            "name": "dither.noise",
            "description": "Add noise bits",
            "type": { "default": 0, "min": 0, "max": 16 },
            "params": true
          },

(apparently i couldnt have used a value larger than 16, but once again, it doesnt matter, because it simply doesnt work)

funny. found someone on reddit (ugh) with the same problem (and on a steam deck too!)

after using it for a bit, I noticed it resets whenever I pause or switch to a different video/tab. Is there a way around this so it always is connected, preferably for all audio sources? thanks

(talking about helvum, but is actually alsa_playback, AGAIN)

i dont know what am i doing wrong. should i be creating this "99-disable-suspend" with services.pipewire.extraConfig.pipewire instead? or services.pipewire.extraConfig.pipewire-pulse? because librewolf uses alsa… does anyone know? but i dont think it has the same syntax as wireplumber… that’d make them redundant! no? ALSO ALSO, yes, pipewire should be the primary audio server (for me), as per services.pipewire.audio.enable (more on that below). and also also also, do i even need to do the double digit numbers for the config file name? they were popular in the pre-json era, nowadays its just one word like shit.conf (we omit the .conf part in nix, right? not a file name issue, right?)

EDIT: ohhhh. 1 mystery solved. pipewire dropped lua for json for its configs since version 0.5, which was about a year ago or so. this includes apply_properties which is now update-props. the more you know!

but i do have a suspicion. i am on a steam deck. it has a pretty wacky audio card (chip) called AMD ACP5x (snd_soc_acp5x_mach). what is even more wacky is that i have to use jovian-nixos (drivers & kernel) flake for a functional GPU that is apparently unusable on the main kernel, and so what it ALSO has is its own pkgs.pipewire and pkgs.wireplumber for some reason. could… that mean anything? EDIT: nope. SORRY, i forgot i have jovian.devices.steamdeck.enableSoundSupport disabled (cos i dont like anyone or anything tinkering with my intricate audio config). EDIT2: i ended up getting rid of jovian-nixos at all, i fixed my GPU freezes and hangs! \BREAK\ …though the fact that the steam deck has a combined jack for mic and headphones (which is exploitable if one were to buy a splitter) and the fact that the aforementioned snd_soc_acp5x_mach module is BOTH an input and an output device (meaning you cant disable the microphone as a kernel module without disabling the whole audio)… AND! if you ever manage to disable the internal microphone (EDIT: i did, will post soon), it defaults to your monitors, because valve, haha, epic (actually amd APU). i would not be surprised if someone told me to just stop, “its not worth it”, ya know?

unless someone could perhaps help me, someone who also does wacky stuff like me with PIPEWIRE? someone who isnt weak and willing to downgrade to pulse/alsa/windows the moment there is an obstacle that is preventing progress. it’s very niche. and every other “tutorial” i’ve read is written in imperative code, i dont understand it. EDIT: speaking of which, its your lucky day imperative distro users, because pipewire uses json (more on that later) for its configs, so it will be pretty easy to understand nix too! (not that nix is like json, no its more like lua, but um, whatever)

for example:

looks promising. but it’s using pactl which is literally pulseaudio, which is afaik superseded by wpctl and pw-link, etc.

and here’s what i’ve been raving about: NODE SUSPENSION (and idle states, which are apparently two different things!). this person made a script that LITERALLY forcefully relinks broken nodes EVERY second. that is absolutely hilarious. so, see what i mean? perhaps by declaratively making alsa_playback to work with my config that specifically prevents this scenario would fix the issue! but nothing works for me at ALL! see?

sorry. i just dont like being misled. it says pipewire is made easy, but nothing makes sense or work…

anyway. here’s what i’m doing (to recreate):

  1. EDIT: not anymore, but previously i was using sonusmix to imperatively create a virtual microphone, now i am using a null sink (that is actually a source???) declaratively, in a config… \ make a virtual device (microphone), which is not actually a device NOR a default input, according to wpctl set-default, but it doesnt matter (i think?). but it really bothers me that i have to use pactl from pkgs.pulseaudio to set it as the default input, instead of wpctl set-default, which requires it to be a device node of media.class (EDIT: apparently this is not implemented, see further below). anyway, worst case scenario, with older games (goldsrc), it captures/defaults to your default input (makes sense), which may not be necessarily your sonusmix virtual input, but actually your internal microphone (which, again, in my case, i am unable to disable) (EDIT: i managed to disable the steam deck’s internal microphone as a device via pipewire entirely WITHOUT tinkering with priorities, will share the config if i will remember to!), so i have to take the L and use pkgs.pulseaudio’s pactl set-default-source, alas
  2. play some music via the browser/player for it to appear as a playback application, because it’s stupid and doesnt know otherwise.
  3. wire/route/link/connect nodes (left, right) from the browser to the virtual microphone (via sonusmix, or via pw-link, or via helvum and the like)
  4. unwire/unlink/disconnect playback nodes from the browser to the headphones (because i dont want to be able to hear music twice with the voice_loopback 1 anyway)
  5. if you didnt notice already, once the music stops, you have to do it all again, sometimes it doesnt even allow you to reconnect, because nothing is working

and this is where i am. the only, like, possible chance of it working is by playing the whole album, a mix, compilation or long tracks, so that you have ENOUGH TIME to relink everything. it’s stupid. it’s annoying. i CANNOT believe this is a thing that is happening… AND NO, this is NOT sonusmix. i’m pretty confident. just like helvum or pw-link, sonusmix also randomly loses connections. it has a feature to prevent sources/sinks from changing node links, but it doesnt seem to work, when the node ITSELF deletes itself, ya know??? so no, it’s not sonusmix, it’s the way playback audio pops in and out of existence. sorry, just had to clarify.

i also tried manually creating null sinks (sonusmix already does that). problem is, it’s not a device, once again, so… ok. im going to try it again after i finish this…

i tried… options snd_soc_acp5x_mach power_save=0 power_save_controller=N in boot.extraModprobeConfig to disable any “power management” for audio, just in case it’s actually the chip being naughty. especially in TLP audio power management (and the like).

what i didnt try is the “pro audio” profile (underdocumented). i think it’s a wee bit overkill for what im trying to do, right? no one’s gonna bother explaining that to me and i’m lazy, so nah, thanks.

so yeah. those with short attention span, once again, i cant manage pipewire’s and/or wireplumber’s extraConfig to work, it seems to not do anything, when i told it to, for example, play a constant stream of noise so that it doesnt unlink the nodes for being “silent” or “idle” or whatever…

tl;dr it is impossible for me to stream my browser’s audio to the microphone without it randomly disconnecting and remembering my patches/links!!!

sorry once again. im just gonna hit the send button…

THANKS IN ADVNCE FOR ANY HELP GUYS!!

p.s. imagine if someone randomly mixed up your patchbay on your eurorack. that’d be hilarious.

EDIT: okay. checked the properties with pkgs.coppwr (check it out!)… so, apparently, the problematic node alsa_playback.librewolf, which is obviously media.class of Stream/Output/Audio… has no "session.suspend-timeout-seconds", nor "node.always-process" properties! it’s not a device, it’s not anything! BUT IT SHOULD BE! when i told "node.name" = "~alsa_playback.*" to target everything with regex!!! angry face.

in other news, APPARENTLY it DOES apply update-props to my other sources and sinks, except sonusmix and the alsa_playback apps (clients)! and EVEN THEN, the properties that it did apply to seem to not do anything? for example, i should be able to hear noise both in my microphone and my headphones, but there is none? the property is there, but it’s just nothing! it’s like they’re sentient or something. okay, i am going deeper and deeper into this rabbithole. it could even be… the fact that playback is always pure alsa that isnt even routed to pipewire? that would be actually insane.

wait a minute, what’s this?

it’s written in pre-0.5 lua though. would no longer work. is this deprecated and misleading information or? EDIT of an EDIT: i WILL confuse wireplumber with pipewire and its versions A LOT. it doesnt matter that much.

EDIT2: forgot to mention that i have the usual options for music production, i do have rtkit, pipewire with wireplumber and its alsa.enable (NOT hardware.alsa.enable!), pulse.enable (NOT services.pulseaudio.enable!), jack.enable (NOT services.jack.*.enable!), so basically pipewire and pipewire-pulse, that’s it. …you know what would be funny? routing everything to JACK as a proxy. lmao

EDIT3: hmm. remember when i said

should i be creating this "99-disable-suspend" with services.pipewire.extraConfig.pipewire instead? or services.pipewire.extraConfig.pipewire-pulse? because librewolf uses alsa… does anyone know?

well, there is one more option that i missed: services.pipewire.extraConfig.client which sounds promising, it’s about APPLICATIONS, so most likely, the ones that are for playback. that is very confusing. wireplumber’s extraConfig can only do so much, apparently. im going to try it after i set up a declarative null sink, one moment please…

EDIT4: okay a bit of progress, but still no way of fixing the node links disconnecting. here’s where i am so far:

the virtual microphone source

  services.pipewire.extraConfig.pipewire."91-null-sinks" = {
    "context.objects" = [ # https://docs.pipewire.org/page_man_pipewire_conf_5.html#pipewire_conf__context_objects
      {
        factory = "adapter";
        args = {
          "factory.name" = "support.null-audio-sink"; # "api.alsa.pcm.source"? -- also couldnt find any documentation on this
          "node.name" = "Microphone-Proxy";
          "node.description" = "micspam";
          "media.class" = "Audio/Source/Virtual"; # "Audio/Source"? -- "Audio/Sink"?
          "audio.position" = "MONO"; # "FL,FR"
          # "priority.driver" = 8000; # sources = 1600-2000; sinks = 600-1000 -- redundant IF the only microphone
          # "priority.session" = 8000; # sources = 1600-2000; sinks = 600-1000 -- redundant IF the only microphone
          "node.dont-fallback" = true; # "node.autoconnect"?
          "object.linger" = true; # keep linked even if destroyed -- DOESNT WORK BTW
          # ...
        };
      }
    ];
  };

the playback client

  services.pipewire.extraConfig.client."95-librewolf-alsa" = {
    "monitor.alsa.rules" = [ # EDIT: AND "stream.rules" !!!
      {
         matches = [
           {
             "node.name" = "alsa_playback.librewolf"; # the browser
           }
           {
             "application.process.binary" = "librewolf"; # just in case
           }
         ];
         actions = {
           update-props = { # aka "apply_preferences"
             "session.suspend-timeout-seconds" = 0;
             "node.pause-on-idle" = false;
             "node.suspend-on-idle" = false;
             "node.always-process" = true;
             "node.autoconnect" = false; # disable automatic linking to headphones (default/best available node)
             # "node.dont-reconnect" = true; # maybe?
             "target.object" = "Microphone-Proxy"; # "node.name" OR "object.serial" -- DOESNT WORK BTW -- they recommend the latter but its actually useless since its always random after the connection is destroyed
             "node.dont-fallback" = true; # "node.autoconnect"?
             "object.linger" = true; # keep linked even if destroyed -- DOESNT WORK BTW
             # ...
           };
         };
      }
    ];
  };

sry i might’ve messed up the syntax, its so late rn.

anyway, so this is as far as i have gotten. it’s a bit easier now to connect stuff to the virtual microphone, but the fact that i still have to do it every time, you know, is suboptimal. oh ALSO, "dither.noise" doesnt work. i DONT understand why. itts very very annoying.

i’ll look into some other stuff tmrw… such as adding more args and preferences to the virtual mic?

EDIT5: okay wow. crazy. so, so, the "target.object" of my stream (client) of the librewolf audio (and NOT vice versa, according to the linking policy, see below), is supposed to connect to "node.name" of my virtual mic but it just doesnt??? cool, but… it doesnt say anywhere that im supposed to also provide a port, yknow, the input/output, left/right/mono? do i have to? i dont know. im figuring this out. here’s what im reading:

https://pipewire.pages.freedesktop.org/wireplumber/policies/linking.html

https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/settings.html

https://docs.pipewire.org/page_streams.html

so yeah. dead end. there is 0 documentation on this, like i said. on ONE HAND you have the "target.object" (i’ve also tried the legacy "node.target", but it obviously didnt work), and on another hand you have deprecated tutorials in lua that tell you that it’s impossible to do via pipewire/wireplumber config.

to my understanding, the ONLY WAY to keep connections alive is to literally run a script in the background which spams pw-link command. that’s hilarious. im so tired…

EDIT6: updated the title to better reflect whats happening

EDIT7: i feel like the title still doesnt represent the content…

Apologies if I am stating the obvious.

What I do for this purpose is I launch mpv with --audio-device=<name>, where <name> is the name of my virtual mic sink. I have a wrapper script around that, which opens a dmenu with all the sounds saved in my soundboard directory.

The script in question
#!/usr/bin/env sh

ls -x1 ~/Music/soundbox | dmenu -i -l 20 | xargs -I {} sh -c "mpv --no-video --audio-device=pipewire/soundboard_sink  ~/Music/soundbox/{} & mpv --no-video --audio-device=pipewire/comms_sink  ~/Music/soundbox/{} & wait"

You might want to adjust this script to your liking, mpv has yt-dlp support IIRC. I also use qpwgraph with its persistent patchbay function to link nodes.

oh yeah. i know. i use mpv everyday, actually, since i try to avoid using google services directly, i use pipeline to look up vids and mpv with yt-dlp (sorry out of topic and i am easily distracted).

but it has the same problem as with any other "media.class" of "Stream/Output/Audio" - it goes “idle” when it’s finished or paused, and the node links just disconnect, again and again.

um, and so your solution connects the mpv stream to the sinks on each sound played, which is fantastic, but i would rather not use any scripts, if you understand, and instead somehow keep nodes linked even if they are destroyed, IDEALLY, by “locking in” to alsa_playback.librewolf FOREVER (EDIT: sorry i meant the virtual mic, not the other way aroun) and delcaratively so, the nix-way, you know? so that it ALWAYS streams to the virtual microphone. but it appears to be impossible…

i dont understand one thing though. should it be a device? the virtual microphone? because it is a "Audio/Source/Virtual", should it be "Audio/Source" instead? it doesnt list "*/Virtual" as a thing in the documentation though…

i used mpv to do the same thing as im doing right now with librewolf, the browser, but it’s literally the same scenario.

and i’ve heard about qpwgraph… didnt try it yet though. what does it do different internally than what im trying to do in a config? …does it also just spam the node link command or something?

damn, i might actually have to write a shell script that literally does just that - spam pw-link.

but thanks, i will do a couple more things until i give up

The reason I am using a script with mpv is because I previously tried something along the lines of what you want to do and gave up, because mpv did (and still does) what I need(ed) it to do. It might be possible to override the default audio sink per application, that way it would always reconnect to the desired sink, although I don’t know how to set that on a per-application basis.
I also wonder what connecting a browser does what mpv doesn’t do. Is it only about the GUI?

I have my sinks configured with nix e.g.

config snippet
          {
            factory = "adapter";
            args = {
              "factory.name" = "support.null-audio-sink";
              "node.name" = "mic_sink";
              "node.description" = "Virtual_Mic";
              "media.class" = "Audio/Source/Virtual";
              "audio.position" = "FL,FR";
            };

I don’t quite remember where I read that it should be */Virtual, but I also seem to have it set that way.
I mentioned qpwgraph because I prefer its UI to helvum etc., sorry if I misled you there.

In my hours of trying to get pipewire and consorts to do what I want (automatic samplerate switching, passive links, the horror!) I have learned that if there is a way something audio related can be accomplished without touching the pipewire/wireplumber config, that path should be taken to avoid frustration.

the examples section

use target.object of node.name, so im doing everything correctly. i think it actually requires the target node to be of a device. maybe that’s why the way i configured librewolf it doesnt connect to the microphone? well then, um, how do i make a virtual microphone a DEVICE?

and the factory = "adapter"; - do you know what taht is? i cant find it anywhere (EDIT: nvm i found it. link is somewhere below)

and like i said, the services.pipewire.extraConfig.client option is the one responsible for applications, the playback, the stream, the Stream/Output/Audio

i still have to read all of this, dont think that im a fast reader or learner haha.

anyway, no, mpv is fine, it’s great, but it’s just… it takes a longer time for mpv to actually load in, fetch some stuff, etc etc (yes even with --vid=no). one day i will have to make a config for “mpv micspam setup”, haha, but not now. RIGHT NOW the priority for me is to actually see if any of this is possible with stream routing, the proof of concept - stream from browser to a virtual mic without the connection constantly dying.

you know what’s confusing though? "factory.name" = "support.null-audio-sink" and "media.class" = "Audio/Source/Virtual" - it… makes no sense. at least to me. one is a sink, other is a source…

and i dont wanna try qpwgraph even if it has “shortcuts” or “bindings” to reconnect stuff again - I DONT wanna do that. i want it DECLARATIVELY, please, in a config, you know.

it is frustrating but it’s something im pretty passionate about. i havent used pipewire on a serious basis enough so that i could arrange my synthesisers and stuff in it (im DAW-less anyway) but THIS micspam scenario is a pretty interesting introduction to how this stuff works… i dont know how this works but im going somewhere

EDIT1: you know what i just realised? the configuration file "monitor.alsa.rules" and "alsa.rules" are two different things…

so you go here

https://docs.pipewire.org/page_config.html

this is the index page by the way. and all of the documentation is talking about the "alsa.rules" config file…

apparently and AS FAR AS I KNOW, "monitor.alsa.rules" is what WIREPLUMBER uses. correct me if im wrong? but i believe something… is working with something partially and creating lots of confusion here…

i mean, all the search results on this property leads to wireplumber here… and it is a alsa_monitor.*… that doesnt sound right.

https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/alsa.html

… so. it thickens. WAIT A SECOND. i think i have accidentally confused where i actually am in MY nix config and was writing to the pipewire’s extraConfig all along. or? but some properties work though! okay… i am going to figure this out AGAIN tomorrow.

EDIT2: nevermind. upon attempting to write to just "alsa.rules" instead of "monitor.alsa.rules", everything just lost its properties. yeah… (edit: scratch that. nvm. some work, some dont)

however, i found this unanswered issue:

(i know its for something else, but also pw related!)

so our suspicions whether we should use "Audio/Source" instead of "Audio/Source/Virtual" were correct. this still hasnt been fixed/implemented in version 0.5, apparently! last time i tried creating a virtual mic that is just a normal source it broke all of my audio… well, that’s (probably) one mystery solved, over 9000 more to go…

at the moment it looks like the declarative config for pipewire is a “one-shot” script, which activates on every session login ONCE, so it obviously cant keep connections alive… MY EXPECTATIONS were high, i thought it is a consistent and reliable way for nodes to search and link AUTOMATICALLY to the configured/declared targets… that i set up… but alas.

:sob:

EDIT3: okay guys i am mixing words, i apologise. SORRY. but "stream.rules" is probably what im looking for, NOT "monitor.alsa.rules", under client configuration files, for STREAMING rules. gosh… or UGH. maybe BOTH???

got a clue. so, here:

https://docs.pipewire.org/page_man_pipewire-client_conf_5.html

is the documentation for native pipewire AND (!) alsa clients. now, the question is… should one declare this in services.pipewire.extraConfig.client or services.pipewire.wireplumber.extraConfig?

EDIT4: this is food for tomorrow but…

https://docs.pipewire.org/page_module_combine_stream.html

could this be the way that i should’ve done it? instead of a null sink?

https://docs.pipewire.org/page_module_example_sink.html

https://docs.pipewire.org/page_module_example_source.html

or maybe i mistook a SOURCE for a SINK? a microphone is a so-called “sink”, right? sorry guys, i am autistic and this did not occur to me… so the guide that is talking about “creating null sinks” actually created a media.class of a SOURCE?!

yep, thats it for today…

EDIT5: OKAY last thing, last thing. I FOUND THIS:

and so there are “single nodes” which are NULL SINKS, and “coupled streams” which is WHAT I NEED, at least it sounds like it, I THINK? will also try changing my null sink from source (apparently) to sink… okay i will do this tomorrow, we’ll see how that works

EDIT6: found this guide

https://alexhorner.cc/linux/stopping-pipewire-wireplumber-s-pdif-and-bluetooth-idle-audio-suspension/

which further confirms that i am doing everything correct… except this one is in the wireplumber config, and i am writing to the pipewire client config (!)… once my long ass update finally compiles, i will try writing to the wireplumber config TODAY

EDIT7: it doesnt work, doesnt even write the properties, so im kinda starting to not understand why wireplumber even exists in the first place if it’s that useless… neither "monitor.alsa.rules" or "stream.rules" for clients… i dunno what to say! except… WHY??? it works for inputs/outputs/sinks/sources, but not clients/streams/playback? the documentation says otherwise… does it not support SOME properties? what am i doing wrong??? looks like the “session manager” cant do its job.

there is ttthis page that i may have already posted

https://julian.pages.freedesktop.org/wireplumber/daemon/configuration/settings.html

that has a few notable props:

"node.stream.restore-target"
"linking.allow-moving-streams"
"linking.follow-default-target"

…aaaand they’re all true by default. the LAST one, however, is interesting. i will try to disable it just for fun, haha!

… it just doesnt write the properties AT ALL!!! ughhhh. but it should support both pipewire properties and wireplumber settings together!

thats a dead end. i’ve tried everything. its over…

EDIT8: choo choo. all aboard the next adventure! found a lua module called "suspend-node.lua" (NOT `“module-suspend-on-idle”, which is from pulseaudio) which i am trying to disable completely. not sure how to do it just yet…

EDIT9: uhm. wtf?

pw-cli unload-module suspend-node
segmentation fault (core dumped)  pw-cli unload-module suspend-node

wait, was it always crashing in the background? oh, so i finally check the logs, huh. but um, the only thing it says is this when the issue that i am having (that is, node disconnecting on idle) happens:

2025-08-21T15:24:38+02:00 deck wireplumber[2170]:
wp-event-dispatcher: <WpAsyncEventHook:0x18798190> failed:
failed to activate item: Object activation aborted: proxy destroyed

anyway, moving on from whatever that is, people are suggesting to “just comment out the script in the config bro”, but the problem is that i am on a nixos and i dont speak imperative? so um, i couldnt find anything about “unloading” modul;es in the docs, so… it’s not going great

EDIT10: oh my god bruh.

It is not yet possible to remove existing modules or to insert a moudule in the array of modules. It is however possible to conditionally load modules with a condition rule.

this is from the pipewire docs… sorry no link this time. i already have like 9000 links already. why dont you give it a try too? their docs are completely straightforward. anyway, i will give a few tries a go, like one would disable an "module.x11.bell"

ohhhh, they mean the "context.properties" = [ { ( "condition" ) } ], ohhhh, yeah. i know how to do that! it’s simple! there’s like a gazillion of examples lying around!

…hold on, what’s this?

Executing Commands

The context.exec section can be used to start arbitrary commands as part of the initialization of the PipeWire program.

okay, interesting. so i can… execute commands when there is a match? OKAY. here we go… let me see, perchance…

nevermind. that’s not what i thought it is. oh fie!

EDIT11: (im so tired, so i scratched the previous idea) sanity check, i tried setting my virtual mic to "media.class" = "Audio/Source" AGAIN, just to check the logs (as a reminder, pipewire docs do not list "Audio/Source/Virtual" as a supported thing, which i’ve mentioned previously…

media.class

The media class is to classify the stream function. Possible values include:

    Video/Source: a producer of video, like a webcam.
    Video/Sink: a consumer of video, like a display window.
    Audio/Source: a source of audio samples like a microphone.
    Audio/Sink: a sink for audio samples, like an audio card.
    Audio/Duplex: a node that is both a sink and a source.
    Stream/Output/Audio: a playback stream.
    Stream/Input/Audio: a capture stream.

and wireplumber says this (in the logs):

aug 21 16:15:41 deck systemd[1937]: Started Multimedia Service Session Manager.
aug 21 16:15:41 deck wireplumber[2200]: spa.alsa: Path Digital PCM is not a volume or mute control
aug 21 16:15:41 deck wireplumber[2200]: spa.alsa: Path Headset Mic is not a volume or mute control
aug 21 16:15:41 deck wireplumber[2200]: spa.alsa: Path Headset Mic is not a volume or mute control
aug 21 16:15:41 deck wireplumber[2200]: spa.alsa: Path Digital PCM is not a volume or mute control
aug 21 16:15:41 deck wireplumber[2200]: spa.alsa: Path Int Mic is not a volume or mute control
aug 21 16:15:41 deck wireplumber[2200]: spa.alsa: Path Int Mic is not a volume or mute control
aug 21 16:21:07 deck wireplumber[2200]: wp-event-dispatcher: <WpAsyncEventHook:0x9240b80> failed: failed to activate item: Object activation aborted: proxy destroyed
aug 21 16:21:13 deck wireplumber[2200]: wp-event-dispatcher: <WpAsyncEventHook:0x9240b80> failed: failed to activate item: Object activation aborted: proxy destroyed
aug 21 16:21:32 deck wireplumber[2200]: wp-event-dispatcher: <WpAsyncEventHook:0x9240b80> failed: failed to activate item: Object activation aborted: proxy destroyed
aug 21 16:21:55 deck wireplumber[2200]: wp-event-dispatcher: <WpAsyncEventHook:0x9240b80> failed: failed to activate item: Object activation aborted: proxy destroyed
aug 21 16:24:26 deck wireplumber[2200]: wp-event-dispatcher: <WpAsyncEventHook:0x9240b80> failed: failed to activate item: Object activation aborted: proxy destroyed

and there are no nodes for any device, so no audio whatsoever…

but it made me think. what if… there is a possibility that "Audio/Source/Virtual" is deprecated? well, im going to find out AFTER i check out ANOTHER property that i didnt know existed called "adapter.auto-port-config", which allows me to CONFIGURE THE PORTS??? that i thought were impossible to configure before… okay… (EDIT: in narrator voice: it did not in fact allow to link the ports directly. there was never such a property, ever)

edit12: sigh. insufferabl.e

tell you what, i tried "adapter.auto-port-config", my virtual mic disappeared as a device, i tried every setting there is, it really is hopeless. then i tried changing "media.class" yet again to "Audio/Duplex" this time, to no avail, unsurprisingly. the only thing that i gotten out of this is a rhythmical beep beep beep sound every like 0.7-0.8 seconds when i listen to a default input. and no, node links are still disconnecting…

and oh yeah, i forgot. i also tried "monitor.node.rules" with "client.name" as a match, but it did not work either. at this point i am once again confused whether i should need the monitor.* part for the config section…

EDIT13: UH OH. something’s not appearing! and it’s the virtual mic! the one and only sound the default input (supposedly my null sink) is producing is a continuous boop boop sound…

nvm. raised its priority ("priority.driver" & "priority.session") a bit higher (8000). i dunno WHAT EXACTLY was overriding it (?) and producing a beeping sound. there are no other microphones or sinks (as i mentioned, i have disabled it, will write it down once relevant…)

but let this not sound accomplished. node idling and suspension are still yet to be conquered. for now though i am exhausted. i am DONE with this (for now)…

edit14: …BUT SUDDENLY!

https://docs.pipewire.org/page_module_loopback.html

loopback. and only when i say this out loud do i remember that i didnt actually try or read the coupled stream thing. i was too obsessed with some other thing that i dont even remember. ANYWAY! looks like i will have to get rid of my null sink and instead… use this loopback module, huh? be right back.

@_@ (idk shocked and exhausted face)

do you know what? oh my god bruhhh. yea so i was right about something being deprecated. uhhhhhhhhhhh. just one moment, please. i need to try the “new method”

huh. so yeah:

Creating links from the config file is difficult because most sinks and ports are dynamically created and might not be available when the config file is processed. It is really only possible to link between completely static node definitions.

It is recommended to use coupled streams to make virtual sinks because they will automatically be linked by the session manager.

would’ve been nice to know earlier. so, COUPLED STREAMS, huh? so, NOT a virtual source “device” that you link audio streaming clients to, but rather a COMBINED stream/source/sink that you THEN use as a default source (microphone)? oh my god why didn’t i think of that, that makes perfect sense, guys, it is my fault for being clearly so blind and ignorant to the manual, haha! anyway. well, i gave it a go:

  services.pipewire.extraConfig.client."91-loopback-microphone" = {
    "context.modules" = [
      {
        name = "libpipewire-module-loopback";
        # "audio.position" = "FL,FR"; # "MONO"?
        args = {
          # "node.description" = "micspam";
          "capture.props" = { # input
            # "audio.position" = "MONO";
            # "stream.dont-remix" = true;
            # "media.class" = "Audio/Sink";
            # "node.name" = "";
            # "node.description" = "";
            # "stream.capture.sink" = true;
            "target.object" = "alsa_playback.librewolf";
            "node.passive" = true; # create passive links to sinks/sources -- useless?
            "node.dont-reconnect" = true; # useless?
          };
          "playback.props" = { # output
            "media.class" = "Audio/Source";
            # "node.name" = "";
            # "node.description" = "";
            # "node.passive" = true;
            # "node.dont-reconnect" = true;
            # "target.object" = "alsa_playback.librewolf";
            "audio.position" = "MONO";
            "priority.driver" = 8000; # otherwise defaults to beep boop sounds
            "priority.session" = 8000; # otherwise defaults to beep boop sounds
          };
        };
      }
    ];
  };

EDIT: revisiting AGAIN for the 9000th time. this is not a “coupled stream” module preferences of which i am editing, this is a loopback module, so two different things. sorry for confusing for the 9001th time.

(after COUNTLESS hours and over a WEEK in total of continuous, monotonous, non-stop mind pumping action, ABSOLUTELY WASTED TIME AND NERVES, this is THE ONLY semi-working config that AUTOMATICALLY connects playback streams (all of them, apparently?) to a virtual source that it is coupled with, which then can be selected in the app/game/chat (or made default by priority)…

idea: i should maybe also globally override the properties of the client module and add a coupled source within the module or something? (TODO: look up pipewire client lua module docs). so that streams spawn as a mono channel, etc, instead of downmixing and routing through loopback devices, for example… hm, this isnt half bad, actually. what if it’s a better solution than introducing a loopback device? huh. well, its a bit too late for that now. uhm, but it really had to be like 5 in the morning for me to come up with this…

and, oh, was it not nice at first. it’s HORRENDOUS!!! it’s horrid. there is like a 50%50 chance that if i shutdown/reboot/logout, it creates like 4 maybe 8 different useless devices, and up to 6 more when librewolf starts playing audio, when i only wanted just ONE, MONO, DEVICE!!!.. im not joking, by the way, i made a screenshot from the patchbay, it was SURREAL, it’s like i connected a 128 channel synthesiser OR SOMETHING. and the only way of like getting rid of all of them is by restarting EVERY SINGLE audio device via

systemctl --user restart wireplumber pipewire pipewire-pulse pipewire-jack

and ONLY THEN do i finally get 1-2 virtual microphones (and i say “1-2” because there literally are 2 virtual microphone devices and i cant do anything about that???

output.loopback-2064-19:capture_MONO
input.loopback-2064-19:monitor_MONO
input.loopback-2064-19:input_MONO

this is the BEST CASE SCENARIO. i am GLAD there are only 3 of them. the sonusmix app i previously mentioned? creates just one input, that’s it, no useless capture or monitor (that doesnt even connect to, like, chats and games and such, so…). and i know there are “default properties”, like for the "media.class" for "capture.props" and "playback.props", but as soon as i change any of their settings, once again, a swarm of 10 useless virtual devices appear out of nowhere, and it’s just, why, you know.

sigh. i think i should just stick with the null sink, though…

yeah, sure, this loopback “module” outputs streaming audio to the virtual source (microphone) that steam and other apps were able to see and stuff… but… when there was no audio playing, e.g. when paused, it makes a skipping noise, like an infinite feedback loop, getting louder and louder, as if the audio was stuck?

EDIT: future me here. apparently it defaults to the MONITORS, your headphones, if you dont set a priority for the microphone. it loopbacks what you hear in general! that is so funny bwahaha.

btw i didnt even bother with suspension and idling. maybe later. it’s just so fragile. im glad it even kinda works. you can break this device (or the order of things) just by listening/playing back something…

speaking of suspension, i have some news on this matter. so, apparently, sometimes it is possible to actually do the opposite of disabling, but rather increasing its timeout value to a very high number, like a full day, which is:

"session.suspend-timeout-seconds" = 86400; # 24 hours

if 0 doesnt disable it, then a full day oughta do it!

then i tried downmixing from stereo to mono, and it just… created 4 different devices and i was just…

and on top of that, the audio was piss poor. kinda expected, but at the same time not? it’s not mentioned anywhere what should one do in case the stream loopback sounds like a soda can (no bass)? which, to be fair, is GOOD for older codecs that old games use in voice chats, because you know, lower frequencies are harder to decode, etc. BUT STILL!

…aaaand there are 2 more issues that i wanna tinker with:

disabling the MPRIS automatic pausing (annoying) of media players and the pipewire property persistency… but i forgot where i got these from, and probably will never gonna find them again…

so. im still not done yet, but this is progress! :crazy_face:

so, let me show you what happens when i am PLAYING audio (streaming) to the virtual microphone which are (finally) connected:

pw-top

S   ID  QUANT   RATE    WAIT    BUSY   W/Q   B/Q  ERR           FORMAT                       NAME
R   76      0      0   3,2us   3,7us  0,00  0,00    0         F32P 1 0  + output.loopback-2452-19
R   69      0      0   7,4us   8,4us  0,00  0,00    0         F32P 2 0  + input.loopback-2452-19
R   71   1200  48000  42,2us  18,1us  0,00  0,00    0    F32LE 2 48000  + alsa_playback.librewolf
R   82      0      0   5,1us  70,0us  0,00  0,00    0         F32P 1 0  + output.loopback-3896-19
R   87      0      0  44,3us  11,4us  0,00  0,00    0         F32P 2 0  + input.loopback-3896-19

(notice how no sample rate, quant or bit depth is accounted for, so the virtual microphone quality is pretty bad… i suppose its routing like a pure data stream or something (makes sense). once again, i dont care BECAUSE the game’s voice audio codec cant handle crazy bandwidths anyway. besides, i do can set them manually, but ive no idea if it will spawn another 20 devices, yknow, haha… but its just weird, because this is an actual example from the docs, and this is what would have happened if they were to route a stream to an upsampled 7.1… which isnt very audiophile-y, i hope they know that. or maybe its broken just for me and actually the steam deck’s audio “card” was the problem all along…)

EDIT: its me from the future, just throwing an idea: perhaps changing the global defaults may help:

  services.pipewire.extraConfig.pipewire."92-wtf-help" = { # /etc/pipewire/pipewire.conf.d
    "context.properties" = {
      "default.clock.rate" = 48000; # 44100?
      "default.clock.quantum" = 32; # 1024/1200?
      "default.clock.min-quantum" = 32;
      "default.clock.max-quantum" = 32;
    };
  };

but this doesnt solve the endianness? real term btw. the loopback devices are in F32P, which is not in fact little endian, big endian, nor reverse endian, but rather… NATIVE endian? neither is it signed, actually. im not big into this much nerdiness, but i do notice that it’s not S32LE nor F32LE, so… yeah. wait a second, can steam deck even do 32 bit? i thought its like 24 or something, wow. anyway, i still have to try this. i just dont have the time…

EDIT: shut up, past me. steam deck has cirrus chip in it, its pretty good (96k). anyway, its not endianness, its actually the “audio format”, THE "audio.format". unfortunately, it proved to be hopeless to change it neither for librewolf nor for the loopbacks. firefox/librewolf is horridly HARDCODED to be in floating, and loopbacks use the target defaults properties… blah blah blah. after all, a browser is not professional software, haha. ugh. i talk about this later…

oh. i probably should say this but, YES, i know that i wont be able to hear any audio from librewolf normally. but once again, this is a proof of concept. i have NEVER tinkered with linux audio systems this seriously, right. i could either simply route it to my headphones manually via some patchbay gui or just comment out the coupled stream thing and update. i would do anything for some voice chat tomfoolery.

and by the way. out of topic, but. speaking of steam deck… funny story, actually. i RMA’d it a few months ago cos the battery inflated, and so i decided to disassemble and disconnect the battery. well, for the next couple of weeks i used the steam deck as usual, with a dock station and the periphery and all, and one of the obvious side effects was… it kept dying every time there was a lot of cpu usage, especially with audio. in fact, the audio was corrupting, in like many weird different ways. i had an external ssd with windows on it, and so in one of the games (if you are curious, richard burns rally it’s called), the audio went permanently corrupted, persistent after reboots and all… it’s as if it was like, stuck in a loop, in memory? it was surreal. at first, some of the cars sounded okay, but then some of the ambient sounds or generic gearbox or braking sounds were all pitch shifted, then, when i loaded other cars just to see if i am just tripping, it “infected” every other sound and it stayed like that forever! i was kinda afraid of checking the files, so i quickly backed everything up and left, wiping everything. so… it can be anything, but the steam deck’s audio chip has not been a pleasant experience… actually, i checked its BIOS, and there is something called “amp control”, which presumably boosts (amplifies) audio DAC volume, which may have caused a spike in power, so it kept killing my deck. then, there was audio bit rate, and by default i believe it was 192khz? which was pretty weird as well. i ended up disabling every power hungry feature in there, including the control ports (joystick, pads, buttons, including volume), but it still kept dying. few days later, got a new deck for free (speaking of which, did you know there was a cheap refurbished 64 gb a few weeks ago?). fwiw, it’s pretty powerful for a handheld x86 device… would’ve been nice if it was on arm though, all that wasted heat and that noisy fan wouldnt have been needed… ANYWAY!

pw-link -iol

output.loopback-2064-19:capture_MONO
input.loopback-2064-19:monitor_MONO
input.loopback-2064-19:input_MONO
output.loopback-2452-19:capture_MONO
input.loopback-2452-19:monitor_FL
input.loopback-2452-19:monitor_FR
input.loopback-2452-19:input_FL
  |<- alsa_playback.librewolf:output_FL
input.loopback-2452-19:input_FR
  |<- alsa_playback.librewolf:output_FR
alsa_playback.librewolf:output_FL
  |-> input.loopback-2452-19:input_FL
  |-> input.loopback-3896-19:input_FL
alsa_playback.librewolf:output_FR
  |-> input.loopback-2452-19:input_FR
  |-> input.loopback-3896-19:input_FR
output.loopback-3896-19:capture_MONO
input.loopback-3896-19:monitor_FL
input.loopback-3896-19:monitor_FR
input.loopback-3896-19:input_FL
  |<- alsa_playback.librewolf:output_FL
input.loopback-3896-19:input_FR
  |<- alsa_playback.librewolf:output_FR

this is the best case scenario (not for me though). this is just 2 of them btw. when i wanted to just route 1 librewolf output to 1 input… there would’ve been up to 10 if i didnt restart the service…

a bunch of, you know, useless ports and nodes and devices. i dont understand this… will understand someday after i take another break, haha!

for now, i really really hope this was somewhat interesting or informative, ya know… still has a few problems to solve, and then i will post THE FINAL config, yeah.

i really wish pipewire was like the standard already. there’s SO much legacy and deprecated stuff everywhere on the internet. this was hell. please correct me if im wrong anywhere, i dont wanna cause any more confusion for the future readers. anyway, i will come back again soon…

:frog:

LETS FUCKING GO!!! sorry for screaming!!!

honorable mention to this little guide:

which explained a few things here and there, specifically that it is actually NOT a good idea to use null sinks or combined/coupled streams for a virtual microphone, and one should instead use loopback modules, hey, like im doing! that i figured out on my own! yeah, i wish i found this tutorial sooner though, but nonetheless, there it is. ANYWAY!

i tinkered a bit (for a few days non-stop again) with the loopback thingie, tried every single configuration and FINALLY found what was causing all the trouble.

here’s the loopback (module) virtual microphone (THIS TIME ITS ONLY 1) which is actually a filter (by wpctl status) but whatever:

  services.pipewire.extraConfig.client."91-loopback-microphone" = {
    "context.modules" = [
      {
        name = "libpipewire-module-loopback";
        # "audio.position" = "MONO"; "FL,FR"
        # ...
        args = {
          # "node.description" = "micspam";
          # ...
          "capture.props" = { # INPUT/MONITOR -- loopbacks FROM librewolf playback/output TO virtual source
            "audio.position" = "MONO"; # "FL,FR"
            # "stream.dont-remix" = true; # destroys nodes?
            # "media.class" = "Audio/Sink"; # useless?
            # "node.name" = "micspam_in"; # creates 2 loopbacks?
            "node.description" = "virtual_in";
            # "stream.capture.sink" = true; # capture speakers/headphones
            "node.target" = "alsa_playback.librewolf"; # where from? -- LEGACY
            "target.object" = "alsa_playback.librewolf"; # where from?
            # "priority.driver" = 8000; # override speakers/headphones
            # "priority.session" = 8000; # override speakers/headphones
            "node.passive" = true; # passive/suspended links
            "node.dont-reconnect" = true; # destroy loopback if unlinked
            # "node.autoconnect" = false; # auto link to defaults
            "node.dont-fallback" = true; # dont link to defaults if no target found
            # "object.linger" = true; # keep linked even if destroyed -- DOESNT WORK?
            # ...
          };
          "playback.props" = { # OUTPUT/CAPTURE -- DONT LINK, select this source in the app/chat
            "media.class" = "Audio/Source"; # "Stream/Output/Audio"? -- "Audio/Source/Virtual"? -- IMPORTANT
            # "node.name" = "micspam_out"; # creates 2 loopbacks?
            "node.description" = "virtual_out";
            # "node.passive" = true; # passive/suspended links
            # "node.target" = ""; # where to? -- LEGACY
            # "target.object" = ""; # where to?
            "audio.position" = "MONO"; # "FL,FR"
            # "stream.dont-remix" = true; # useless?
            "priority.driver" = 8000; # default = 1500; sources = 1600-2000; sinks = 600-1000 -- otherwise defaults to beep boop sounds
            "priority.session" = 8000; # default = 1500; sources = 1600-2000; sinks = 600-1000 -- otherwise defaults to beep boop sounds
            # "node.dont-reconnect" = true; # destroy loopback if unlinked
            # "node.autoconnect" = false; # auto link to defaults
            # "node.dont-fallback" = true; # uncomment?
            # "object.linger" = true; # keep linked even if destroyed -- DOESNT WORK?
            # ...
          };
        };
      }
    ];
  };

(oh by the way, dont change the "media.class" of the playback (capture) node to "Stream/Output/Audio", like the guide above says so, it just doesnt work. yeah… ALSO not "Audio/Source/Virtual", as i previously mentioned… though it was a null sink, idk if a loopback module can accept this class. better leave it as just A source…

(if one wants to route audio from something else besides alsa_playback.librewolf, then just look up its "node.name" string in pkgs.coppwr or via pw-top, e.g. mpv would be mpv (lol), and change the capture’s "target.object" to that! there is also "object.serial", but its completely USELESS with DYNAMIC nodes!!! i mean, just look at my example. there is a regex you can use to target multiple nodes/objects by whatever…)

so, the solution was/is… "node.dont-fallback"true that and it wont create 69000 devices! yay! there is also a "node.autoconnect" = false which makes nodes stop connecting to your speakers/headphones, but this property should be set directly to the client/application/stream, so you also need this:

  services.pipewire.extraConfig.client."95-librewolf-stream" = {
    "monitor.stream.rules" = [
      {
        matches = [
          {
            "node.name" = "alsa_playback.librewolf"; # "~alsa_playback.*"?
          }
          {
            "application.process.binary" = "librewolf";
          }
          {
            "application.name" = "~librewolf.*";
          }
        ];
        actions = {
          update-props = {
            "session.suspend-timeout-seconds" = 0; # 86400? -- doesnt work, but just in case
            "node.pause-on-idle" = false; # doesnt work, but just in case
            "node.suspend-on-idle" = false; # doesnt work, but just in case
            "node.always-process" = true; # process even if unlinked
            "node.autoconnect" = false; # auto link to default or best available SINK i.e. your speakers/headphones
            "object.linger" = true; # keep linked even if destroyed
            # "node.dont-fallback" = true;
            # "node.dont-reconnect" = true;
            # "dither.method" = "wannamaker3"; # "rectangular", "triangular", "triangular-hf", "wannamaker3", "shaped5"
            # "dither.noise" = 10; # integer
            # "audio.position" = "MONO"; # "FL,FR"?
            # "node.force-rate" = 22050; # 48000?
            # "audio.format" = "S32LE"; # doesnt work
          };
        };
      }
    ];
  };

and last but not least, skip this next optional config if you dont have a steam deck and/or dont care about an open microphone always in the background:

  services.pipewire.wireplumber.extraConfig."97-disable-devices" = {
    "monitor.alsa.rules" = [
      {
        matches = [
          /*
          {
            # "alsa.driver_name" = "snd_soc_acp5x_mach"; # disables steam deck's audio kernel module -- "boot.extraModprobeConfig"?
          }
          {
            # "device.name" = "~alsa_card.*";
          }
          */
          {
            "node.name" = "alsa_input.pci-0000_04_00.5-platform-acp5x_mach.0.HiFi__Mic__source"; # disables steam deck's internal microphone
          }
        ];
        actions = {
          update-props = {
            "device.disabled" = true;
            "node.disabled" = true;
          };
        };
      }
    ];
  };

this makes it easier to set/select a default (virtual) microphone (source)… as well as it disables your internal microphone, duh!

EDIT: hey its future me. remember i said that steam deck has a combined jack for headphones and a microphone? yeah, so if you disable the INTERNAL microphone, then the microphone becomes your HEADPHONES, specifically, its monitor. so your output becomes your input. this could be either good or bad. so, BE AWARE of this factoid!

THATS IT! that is all one would need for a nice little loopback virtual microphone. thanks everyone, goodbye! love you!

keep reading, however, if you are still curious.

all right. SO! every other preference i tried (that is commented out) for the loopback module is seemingly breaking things and generally making everything worse, so… DONT TOUCH ANYTHING! (you can, but…)

but other prefs/props just dont work AT ALL. such as… "audio.format"… NOPE, no way of fixing the shitty audio quality! the difference (F32LE vs F32P) in the format makes the lows (bass) practically cut out:

here it is, btw, finally working:

pw-top

S   ID  QUANT   RATE    WAIT    BUSY   W/Q   B/Q  ERR           FORMAT                       NAME
R   37      0      0   2,0us   4,5us  0,00  0,00    0         F32P 1 0  + output.loopback-2158-19
R   49      0      0   7,1us   8,7us  0,00  0,00    0         F32P 1 0  + input.loopback-2158-19
R   50   1200  48000  33,5us  19,5us  0,00  0,00    0    F32LE 2 48000  + alsa_playback.librewolf
R   76      0      0   2,7us   4,4us  0,00  0,00    0         F32P 1 0  + output.loopback-2949-19
R   75      0      0  18,1us  10,1us  0,00  0,00    0         F32P 1 0  + input.loopback-2949-19

(the second loopback is actually me checking in on the audio situation via pw-top… i cant wrap my head around this, but it multiplies itself when you are checking devices’ status! it deletes itself once you exit pw-top, lol…)

you can see that librewolf is F32LE FLOATING little endian, and loopbacks are in F32P which is… floating, uh, nothing? wtf even is that?

by the way, i DID try to also set:

  "default.clock.rate" = 48000;
  "default.clock.quantum" = 32; # 1024/1200?
  "default.clock.min-quantum" = 32;
  "default.clock.max-quantum" = 32; # 1200?

as well as

  "clock.force-rate" = 48000;
  "clock.force-quantum" = 32; # 1024/1200?
  "node.force-rate" = 48000;
  "node.force-quantum" = 32; # 1024/1200?

AND

  "audio.format" = "S32LE"; # S32LE > F32LE

and… it didnt work, wow.

neither in
services.pipewire.extraConfig.client
nor in
services.pipewire.wireplumber.extraConfig

the sound is still shit! in fact, when i tried setting the sample rate, audio became noisy and crackly. as expected when the formats ARE DIFFERENT and INCOMPATIBLE! and they are, by default, for SOME unknown reason… assumingly, they default to 0, which tells it to use the target’s default props, you know, to “match” them hopefully… but i cant change such property… ffs.

here’s librewolf linked to my headphones:

S   ID  QUANT   RATE    WAIT    BUSY   W/Q   B/Q  ERR           FORMAT                                                                      NAME
R   62   1024  48000 176,0us  18,4us  0,01  0,00    0    S32LE 2 48000 alsa_output.pci-0000_04_00.5-platform-acp5x_mach.0.HiFi__Headphones__sink
R   68      0      0   3,3us   6,2us  0,00  0,00    0         F32P 1 0  + output.loopback-3283-19
R   36      0      0   6,9us  13,5us  0,00  0,00    0         F32P 1 0  + input.loopback-3283-19
R   48   1200  48000  52,2us  28,3us  0,00  0,00    0    F32LE 2 48000  + alsa_playback.librewolf

see? there’s like… an obvious problem here… that i dont know yet how to fix…

EDIT: could it be because of the downmixing to MONO? maybe. but that’s ridiculous… SURELY NOT, right? okay, will check it TOMORROW.

EDIT2: forgot to check tomorrow. did it the day after. NO IT DOESNT MAKE A DIFFERENCE. waste of time.

so i searched around, and found this:

SURPRISE! they recommend using a null sink! bwahahahaaha! so, routing a loopback to a null sink, huh?! no, thank you. right… null sink by itself, however, has perfect sound. iirc it is F32LE? doesnt matter, it doesnt sound like someone forgot to plug the aux fully in, at least.

i think this is as best as it will ever get for now!

so, what does not work:

  • disabling node suspension
  • changing audio.format

and that SUCKS. it really just blows. pipewire and wireplumber? i dunno, man, seems kinda not worky, but i dont wanna use anything else either!

ugh.

but it kinda works though!.. so, there you (plural) go… i guess…

at the end of the day, hey, this isnt just for “micspam”, ya know! this is a professional solution for software audio patching! i know its easier with physical hardware…

addendum:

hey @BENDI , hope youre doing well. i found this option for mpv.conf:

audio-stream-silence=yes

which does exactly what you think it does. hope this helps, cheers!

edit9999 (idk): HEY there, future me, i might have been confusing alsa_output.pci-0000_04_00.5-platform-acp5x_mach.0.HiFi__Headphones__sink with alsa_loopback_device.alsa_output.pci-0000_04_00.5-platform-acp5x_mach.0.HiFi__Speaker__sink and alsa_output.pci-0000_04_00.5-platform-acp5x_mach.0.HiFi__Speaker__sink, two different things actually (no idea what the former one is from), long story short, i am stupid, as per usual, i dont remember if i mentioned this anywhere, but speakers/headphones/output = SINKS, microphones/cameras/input = SOURCES, but just to be sure again, yeah, could be just a quirk of the steam deck’s DAC or whatever, right, oh wow, what a mess this page is, sheesh, byeee

also, here’s a few useful commands for debugging this mess:

wpctl status --name
pw-mon --hide-params --print-separator
pw-link --input --output --links --monitor --verbose
1 Like

Hey, thank you for pointing out that configuration option, but I don’t think it would be an improvement in my case. Mpv starts fast enough for me to just launch another instance every time I want to play something. As mpv automatically connects to the appropriate sink, I do not see how I would benefit from keeping the session alive by just playing silence. Sucks that you weren’t fully able to accomplish what you wanted.

What I don’t get however is how you arrived at the conclusion that setting up null sinks is not something one should do. The article advises against using libpipewire-module-combine-stream, but the only criticism of null sinks I found there was that setting them up is supposedly very hard. I never encountered the issues you described (my null sinks have always had source and sink ports).

I am also surprised that setting target.object to something which doesn’t exist at evaluation time works. I thought only the session manager is able connect nodes dynamically?

As for changing the audio “quality”, my experiences mirror your own: I was only ever able to change the samplerate by overriding it via the cli. This is actually why all of my virtual audio devices are linked with passive links[1] which is needed to get pipewire’s builtin dynamic sample rate switching to work.

I guess finding numerous config options which seem to do what one wants, only for them to not do anything is just the experience of configuring pipewire…

[1]PipeWire: pw-link

hmm. fair!

haha, its not over yet!

uhm, well, as i mentioned previously, i have come to a conclusion, after many variations of my config and days of testing, that it is impossible to automatically link null sinks, simple as that, really… and the guide just confirmed it further.

i have no idee how that works either, but, HEY, it works, alright, thats all i need, hahahaa

hmmmm. yeah. this is the biggest mystery so far. maybe its literally impossible as of this version. maybe you have to route it to JACK or somethjing and configure stuff there. i really dont know. nobody does i believe?

yeah. it was very misleading, confusing and annoying. a bit of weird too! its unfortunate, that, that in my case, librewolf, is not a native pipewire application, so there is no native bit rate/format for the loopback module.

but what if i could configure librewolf internally?

maybe there is some media.* property in about:config or something that disallows bitperfect/matching audio formats? you kjnow, for security reasons!

to be honest, i did not try with chromium based browsers yet. i just dont have them…

well, i found this:

this is the only relevant result i found when searching for librewolf’s audio format. (hey, they’re using nix btw!)

but that’s not it…! dont think youtube player is using webrtc? ugh. and it’s not librewolf’s fault either. mpv stream is also using F32P floating point format… cos at first i was like “oh, well, it’s mozilla then, cant get their media players set up proper”, but no, it looks like all audio playback streams are like this? yeah! huh. but this is very strange isnt it? all other streams sound fine in F32P, but when you route it to a loopback it isnt? but loopbacks dont even have such properties, they just copy them from the node they’re linked to…

well! looks like there’s a lot of things left to do… im thinking first tinker with the loopback module (again), then "context.properties", "monitor.stream.rules" and "monitor.stream.properties" are the next sections to bother (still dont know if i need the monitor.* part or not!).

EDIT: maybe it’s these little guys responsible for the audio quality worsening:

"node.always-process" (see below)
"resample.disable" (only if you set sample/bit/quant/buffer rates, etc. props manually!)
or
"resample.quality" (maybe hopeless?)

specifically the “resampler parameters” subsection in “audio adapter properties” section:

https://docs.pipewire.org/page_man_pipewire-props_7.html#props__audio_adapter_properties

(btw despite them saying “these are node properties” this DOES NOT MEAN that this is a "monitor.node.properties" section or something, it’s just that, AS USUAL, the docs are very, very very very very, very confusing. these can be applied to "capture.props" and "playback.props" of the loopback module TOO!)

and apparently "audio.format" is ALWAYS F32P for streaming client nodes because “the graph processing format is always float 32.

huh. good to know! thanks, pipewire.

:frog: :+1:

EDIT2: but then!

dither.noise

Note that PipeWire uses floating point operations with 24 bits precission for all of the audio processing. Conversion to 24 bits integer sample formats is lossless and conversion to 32 bits integer sample formats are simply padded with 0 bits at the end. This means that the dither noise is always only in the 24 most significant bits.

was this there before? i dont remember… does this work only with S24LE/F24LE formats, then? damn, if only audio streams WERE in such a format, or were able to change their format AT ALL, am i right?

well… will look at this someday again…

EDIT3: i still have yet to test ALL OF THAT but i also found this:

  programs.firefox.wrapperConfig.pipewireSupport = true;

why is this not the default? guys?

EDIT4: to edit number 2 - welly welly well. leaving this here for my future self, cos i will forget: looks like the gecko protrudes its tongue in a floating 32 bits, have a load of this:

pw-cli list-objects

        id 49, type PipeWire:Interface:Port/3
                object.serial = "75"
                object.path = "LibreWolf:output_0"
                format.dsp = "32 bit float mono audio"
                node.id = "57"
                audio.channel = "FL"
                port.id = "0"
                port.name = "output_FL"
                port.direction = "out"
                port.alias = "LibreWolf:output_FL"
                port.group = "stream.0"
        id 60, type PipeWire:Interface:Port/3
                object.serial = "76"
                object.path = "LibreWolf:output_1"
                format.dsp = "32 bit float mono audio"
                node.id = "57"
                audio.channel = "FR"
                port.id = "1"
                port.name = "output_FR"
                port.direction = "out"
                port.alias = "LibreWolf:output_FR"
                port.group = "stream.0"

format.dsp that is (it has no documentation).

i believe it is a part of "adapter.auto-port-config", which is (kinda) documented here:

https://docs.pipewire.org/page_man_pipewire-props_7.html#props__audio_adapter_properties

but ive mentioned it already somewhere up there. its completely useless. once again, for the 9000th time, for those who are just scrolling by, it is IMPOSSIBLE to change the sample or bit depth of a client (application outputting sound), unless its some kind of a DAW or a game that can switch between lo and hi fidelity audio. now ive said this for the 9001st time, great.

what is left for me to do is to figure out for real this time how to make the noise maker thing work in order to prevent clients-to-playback suspension/idling??? am i missing some noise/dither library or something, actually? ughhh. just THINKING about trying to make this actually function is making me so sleepy, i cannot, like, this is obviously not my problem, someone else messed this up, but i cant even IMAGINE how do people do this, everyday, without taking a snack break every 5 minutes. LOVE me some fking software patching, WOWIE!!! wahoo! lets a go, pipe-wire! we make-a it hard-a to music, yes