Skip to content

PipeWire link hotplug - #16772

Merged
daschuer merged 16 commits into
mixxxdj:mainfrom
pri-yan-shu:hotplug
Aug 16, 2026
Merged

PipeWire link hotplug#16772
daschuer merged 16 commits into
mixxxdj:mainfrom
pri-yan-shu:hotplug

Conversation

@pri-yan-shu

Copy link
Copy Markdown
Contributor

Preemptively allocate all input buffers, and detect link connections and configure/unconfigure AudioSource/AudioDestination appropriately.

Instead of internally copying the buffers, now connect both FL and
FR channels of Mixxx input to the single channel of the SoundDevice.

The behaviour is unchanged for outputs, now instead of checking the
ChannelGroup for picking the port to output audio to, we query both
ports for buffers, in case of single connection we only get buffer
on one port.
@pri-yan-shu
pri-yan-shu marked this pull request as draft July 25, 2026 00:49
@pri-yan-shu pri-yan-shu changed the title [WIP] Add PipeWire link hotplug PipeWire link hotplug Jul 25, 2026
@pri-yan-shu
pri-yan-shu force-pushed the hotplug branch 3 times, most recently from f4f2d89 to cd84efa Compare July 25, 2026 03:35
Comment thread src/soundio/soundmanager.h Outdated
CSAMPLE* getInputBuffer(const AudioInput& input) {
if (!m_inputBuffers.contains(input)) {
qWarning() << "getInputBuffer does not have" << input.getString();
for (const auto& [input, buffer] : m_inputBuffers.asKeyValueRange()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately this does not compile with QT 6.2

Suggested change
for (const auto& [input, buffer] : m_inputBuffers.asKeyValueRange()) {
for (auto it = m_inputBuffers.keyValueBegin(); it != m_inputBuffers.keyValueEnd(); ++it) {
const auto& [input, buffer] = *it;

@daschuer

Copy link
Copy Markdown
Member

Just did some test.

  • qpwgraph did not close when you close the window it continues head-less reconnecting Mixxx when it reopens.
  • The "patchbay" format is quite straight forward.
  • qpwgraph immeditaly reconnects/reset settings form MIxxx GUI

Just some wild thoughts (maybe bad) For the pachbay mode:

  • qpwgraph is availlable on all distros.
  • Add a pakage dependency on it.
  • start "qpwgraph -m &" along with mixxx, if not found in "ps -A"
  • Rely on its functionality and do not invent another patch bay inside Mixxx
  • It already supports different profiles

We may also just copy the Patchbay code into Mixxxx, it is also QT, because we will end up with pretty much the same if we do our own patch bay. But "There Can Be Only One", the "Higlander" issue. Do you know the movie:
https://www.youtube.com/watch?v=sqcLjcSloXs (1986)


Default behavior of Mixxx, like Firefox, If Mixxx has no connection, connect default sink to Main output.

Detect "exclusive mode" of the patch bay and gray out the Mixxx options in that case.

Remove Pop up in case Mixxx is managed by a Patchbay.

@pri-yan-shu

Copy link
Copy Markdown
Contributor Author

I kept the preference page, now we have a checkbox at the bottom (can be moved to a separate pipewire specific region). If it is checked, Mixxx UI tracks external links and updates accordingly, and all connections to any input/output path are handled. If unchecked, Mixxx UI is static, and external connections are untouched.

There are still some edge cases, when checkbox is checked/unchecked while some connections are still made, and it enters invalid state, and preference page is not updated correctly (if it cannot be solved we can make this startup only value).

Also currently routing multiple devices to single input/output is removed when using the preference page UI (Mixxx does not support multiple device routing anyway, but it works fine with patchbay only).

@daschuer

Copy link
Copy Markdown
Member

Just tested a bit and got an assertion violation after removing a connection in the patch bay.

critical [0x7b224c003920] DEBUG ASSERT: "connectedDevices.contains(deviceId)" in function bool PipewireEnumerator::PortPair::removePort(uint32_t, uint32_t, bool) at ./src/soundio/pipewireenumerator.cpp:1164

I have connected the deck output with the patch bay, added the Main output via Mixxx and removed the main output again via the patch bay.

I also noted a "flanger" effect. The deck has a phase offset compared to the main output. Not sure what is doing this.
I have disabled the EQs.

@pri-yan-shu

pri-yan-shu commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Do you have "Sync with external patchbay" checked on or off? I tried following your steps but couldn't recreate the assert violation. Do you have rest of the log?

I also noted a "flanger" effect. The deck has a phase offset compared to the main output.

Is this reproducible? I have also noticed this couple of times, even before this PR, when the sound is much wider than normal, maybe due to a delay. After disconnecting and reconnecting links it goes away, maybe its a PipeWire quirk. But I have only noticed it with Mixxx.

@daschuer

Copy link
Copy Markdown
Member

I need to investigate the flanger thing. A first test would be to compare the output buffers of deck and main.
Maybe it is Mixxx's fault.

@daschuer

Copy link
Copy Markdown
Member

Ups, I actually did not notice the switch. So I guess it was off. What is our strategy for such users like me?

I am afraid we have the use cases not yet well thought.

@pri-yan-shu

pri-yan-shu commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

When the "Sync with external patchbay" checkbox is off (the default), Mixxx operates like it used to do, where the preference page is unaware of patchbay changes. One improvement is that on selecting devices, patchbay connections are untouched.

When checkbox is on, Mixxx UI is synced with patchbay changes, and selecting devices affect patchbay links as well.

Both modes should support editing connections from patchbay and Mixxx UI alternatingly, although changing the checkbox at runtime is not tested thouroughly, I'm debating if we make it startup only (like PipeWire API checkbox) and avoid some effort.

I was unable to recreate the assert violation myself. It could be a specific case I missed. If you have logs it will help to pinpoint the cause.

@daschuer

Copy link
Copy Markdown
Member

I think Mixxx can't prevent that a user uses the patch bay anyway.

How about this start up flow.

  • Start up Mixxx
  • Announce all Ports to pipewire, dont connect
  • Wait 2 s to give a patchbachbay time to configure Mixxx
  • If after two seconds nothing is configures apply a saved configuration
    • We can be smart and apply it only if the required sinks are there.
  • If we have no saved configuration connect main output to default like Firefox

In Mixxx we can have a new Cobobox:
Patchbay:

  • externally
  • default
  • Config1
  • Config2

If Mixxx detects any changes that does not match the current setting switch to "externally"

Use cases:

  1. Ubbuntu Studio, User has a patchbay in his tray
  2. Desktop Linux, Mixxx will follow the configures sound output like Firefox
  3. DJ Setup, a fixed setup shall be enforces without surprises.

@pri-yan-shu

pri-yan-shu commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

About setting default output, it is the session manager's (wireplumber) responsibility to handle default input/output (which you manually set with wpctl set-default ID). With pw_stream API (which Firefox also uses through PulseAudio compat) you can specify PW_STREAM_FLAG_AUTOCONNECT, and it works as intended. For the API we are using (pw_filter), there is PW_KEY_NODE_AUTOCONNECT used in its example, but it doesn't seem to autoconnect, atleast testing on my local machine (I might need to check the wireplumber scripts responsible for that, if we're missing some property or metadata in our node). We can have the default output selection like we do in PortAudio.

Of the use cases you mention, the Ubuntu Studio analogue is currently "Sync with external patchbay" mode, where we sync the UI to external patchbay. If you turn that off, Mixxx handles like the 3rd case, where we use Mixxx UI and depend on Mixxx configuration file. Even when we get the "default output" working, that will work fine with "Sync with external patchbay", and user can opt in if they want to connect to default output.

Is there any usecase this current setup missing? One point I can see is completely disabling Mixxx own config, but that can be a systemwide checkbox, not just PipeWire only.

@daschuer

daschuer commented Aug 1, 2026

Copy link
Copy Markdown
Member

What's your plan with this PR?

@pri-yan-shu

Copy link
Copy Markdown
Contributor Author

About the workflow you mentioned, we have these cases:

  • use patchbay for connection: Here we sync Mixxx UI. We might entirely depend on patchbay config, and disable Mixxx configuration loading, or not if we only use patchbay for editing and not loading config.
  • connect to default ouptut: I did find a way to detect default inputs/outputs, and we can connect to them manually (although in Firefox or other program's cases it is done by wireplumber, the effect will be the same). This can be a checkbox as well.
  • use Mixxx UI: The patchbay will have effect anyway, but we can keep Mixxx UI static, and don't touch external connections, so if someone wants one-off connection, like routing the Main output to pw-record or some other program, its possible without affecting the Main output.

So we have a total of 3 checkboxes,

  • sync external events with Mixxx UI (we already have it, need to fix the assert violation you mentioned)
  • load pipewire default source/sink (in case no devices are open)
  • disable Mixxx config loading (simple boolean condition)

Is there any usecase that is not addressed? Or any reason you feel this is not optimal?

@daschuer

daschuer commented Aug 2, 2026

Copy link
Copy Markdown
Member

I feel it should be more automated, cooperative.
I have played a bit with qpwgraph.
It can be run in standby mode doing nothing or it can save a patchbay and immediately enforced the connections. In that mode you can save connections done in Mixxx to the patchbay and enforce it.

Thats why the timer idea comes up, with no check box.
This gives any external patchbay the chance to wire Mixxx. If it does not happen Mixxx need to care for itself.

@pri-yan-shu

pri-yan-shu commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

So the current state is:

  • Sync patchbay checkbox is still there, uncheck if you want Mixxx to ignore patchbay changes (if you're not using patchbay as main routing application, and only for one-off connections).
  • Mixxx waits for 3 seconds for any active patchbay to connect Mixxx ports. If no output is connected, load Mixxx config (soundconfig.xml). Should I extend it to no input/output is connected (that messes up the logic in next point)?
  • Not currently implemented, but a default device will be added, which connects to PipeWire default source/sink, and will automatically update connections as the default device changes (bluetooth device connects/disconnects). If no Mixxx output is connected (either from patchbay or Mixxx config), then Main is connected to default output.

Is this fine?

@pri-yan-shu

Copy link
Copy Markdown
Contributor Author

The current Mixxx preference page behaviour to patchbay changes (only happens with sync patchbay checkbox on) is a bit confusing and undocumented, so I'll explain here, and provide feedback if it's good or not.

  • If a single device is connected with a channel config which is not representable in Mixxx UI (i.e. which cannot be created from Mixxx UI itself, like non contiguous device ports, or specific cases like only left device output connected to left Mixxx input), then Mixxx blanks out the channel combobox. This seems good, since we are expressing that the given device is connected, but the channel config is not supported, and user can easily select a supported channel config.

  • If multiple devices are connected, then Mixxx blanks out the channel combobox, and the device combobox remains to the first device which was connected. In case that device is completely disconnected, then the device after it is shown. This is not good, since it doesn't convery properly that multiple devices are connected, but the user can go from here to a valid config easily, by either selecting a valid channel or a different device. I cannot set it to the unselected item "---" since then user cannot unselect that AudioPath (since it is already at "---"). Would it be better to automatically add an "--multiple--" item, and remove it if routing goes to single device?

@daschuer

daschuer commented Aug 7, 2026

Copy link
Copy Markdown
Member

Is this fine?

Unsure.

This is the version where the "no sound configured" popup box will be retired. We loose the option to run Mixxx in broadcasting only mode. We have still a popup if users enable microphones with no mic configured. Maybe we want that for headphones as well.

The alternative is to keep that Popup and do nothing on the first run like before.

For now I prefer the proposed version. So we can implement it and decide during the beta if it was a good decision or not.

@daschuer

daschuer commented Aug 7, 2026

Copy link
Copy Markdown
Member

All your considerations are well thought. Thank you. Our biggest challenge is however the timeline. If we try to fix all the nitty gritty details with the drafted cooperative mode, we might miss our schedule.
I tested the code, and there are still a few corner cases to consider. Since we want to merge this tomorrow, let’s strip down the scope of this PR for now.

Can we please focus only on these two simple modes, that likely cover 90 % of all use case?

  • External Patchbay Mode:
    • Gray out or hide all connection options
    • Don't attempt to visualize anything.
    • Don't touch connections at all from Mixxx.
  • Internal routing Mode:
    • Enable/show the port mapping tabs
    • Keep only the connections the user has selected form Mixxx
    • When "Apply" remove external connections
    • Then only apply the Mixxx configured values.

Let's allow to connect additional ports with a patchbay after "Apply" but just ignore them in the Mixxx preferences. If users "Apply" again in Internal mode shall remove external connections again.

This gives a solid base line for everything else.

The mixed mode attempt is still nice for a future PR, but needs some love to have all corner cased right.

@daschuer

daschuer commented Aug 7, 2026

Copy link
Copy Markdown
Member

Would it be better to automatically add an "--multiple--" item, and remove it if routing goes to single device?

That seems to be reasonable (in a future PR). Maybe something more generic because it can also be a crazy routing with single channel. Just to mention swapping FL and FR.

@pri-yan-shu

Copy link
Copy Markdown
Contributor Author

now pipewire_patchbay_sync can be changed at runtime, and config is loaded anyway, just not applied if using external patchbay.

@daschuer

Copy link
Copy Markdown
Member

Just tested this:

  • Sync with external patchbay off
  • to connect main with Mixxx.
  • remove main connection with the patch bay and reconnectt heasphones
  • Apply again.
    -> main is connected in addition (OK)
    -> headphone connection is not removed (Not OK)

@daschuer

Copy link
Copy Markdown
Member

The rest work quite nice.

@pri-yan-shu

Copy link
Copy Markdown
Contributor Author

One observation, when patchbay is active, even after removing the connections, patchbay reconnects, but we cannot do anything about it. Now all connections are removed, so even if internally the device configuration does not change, but externally connections were made, those connections are removed.

@daschuer

Copy link
Copy Markdown
Member

but we cannot do anything about it.

That's OK it still feels natural. The whole PR work good now. I think the "Sync" checkbox should be moved up near the "Query Device".

@daschuer

Copy link
Copy Markdown
Member

Pipewire works great now. Unfortunately storing the setting in ALSA mode is broken now.

@pri-yan-shu

Copy link
Copy Markdown
Contributor Author

This is fixed, thanks.

@daschuer daschuer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First chunk of review comments:

Comment thread src/soundio/pipewireenumerator.h Outdated
std::unordered_map<AudioOutput, PortPair> m_outputs;

PollingControlProxy m_audioLatencyUsage;
ControlObject m_COmanageExternalLinks;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The var name doe no longer match the CO name:

Suggested change
ControlObject m_COmanageExternalLinks;
ControlObject m_coPipeWirePatchbaySync;

@pri-yan-shu pri-yan-shu Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Name needs to be same as ConfigKey? otherwise PipeWire is redundant here.
If need to be same, do we only capitalise at '_' in snake case form, so should it be m_coPipewirePatchbaySync.

Comment thread src/soundio/pipewireenumerator.cpp Outdated

connect(this, &PipewireEnumerator::deviceAdded, m_pSoundManager, &SoundManager::addDevice);
connect(this, &PipewireEnumerator::deviceRemoved, m_pSoundManager, &SoundManager::removeDevice);
connect(&m_COmanageExternalLinks, &ControlObject::valueChanged, this, [this](double value) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get() on a control object is already threadsafe and atomic. So we can dispose the m_manageExternalLinks bool and use directly the CO

qWarning() << "PipewireEnumerator::initialize pw_context_new "
"failed with error:"
<< spa_strerror(errno);
qDebug() << "PipewireEnumerator::initialize pw_context_new "

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This, needs a source code comment. When can it happen? Is a qDebug message the right choice or do we want to inform the user better, along with a call for action?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It fails if no valid pipewire config exists, or PW_KEY_CONFIG_OVERRIDE_NAME has invalid name (doesn't end in .conf) or it doesn't exist, or any pipewire function/libc function fails (like calloc/mmap), or config cannot be parsed. Create a popup with spa_strerror(errno) so user can see if the error is due to something they can control?

qWarning() << "PipewireEnumerator::initialize pw_context_connect "
"failed with error:"
<< spa_strerror(errno);
qDebug() << "PipewireEnumerator::initialize pw_context_connect "

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same here. If we don't expect that this ever fails, we can even user a VERIFY_OR_DEBUG_ASSERR()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can fail for many reasons, but one is if pipewire server is not running, so assert might not be optimal. Create a popup?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If server is not running, spa_strerror gives: Unknown error -112. Not helpful, but probably because there might be multiple causes for this error.

Comment thread src/soundio/pipewireenumerator.cpp Outdated

for (auto it = m_inputs.begin(); it != m_inputs.end(); ++it) {
it.value() = createInputPorts(it.key());
createInputPorts(it->first, it->second);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is hard to read, because of using a Pair.
Is it possible to replace it with a struct or at least a Structured bindings?

Comment thread src/soundio/pipewireenumerator.cpp Outdated
Comment on lines +106 to +109
connect(&m_COmanageExternalLinks, &ControlObject::valueChanged, this, [this](double value) {
// TODO(pri-yan-shu) cleanup any invalid state here, or make the toggle startup only
m_manageExternalLinks = static_cast<bool>(value);
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the comment is now resolved, because the setting is grayed out when sync is disabled. Not sure if we want an extra safety net?

<< spa_strerror(res);
}

m_coreSyncSeq = pw_core_sync(m_pPwCore, PW_ID_CORE, 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this fail?
I have not fully understand how exactly it works. Maybe a source code comment will help.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is to synchronize configuration loading after pipewire device enumeration is complete, this makes pipewire server emit a callback after all the graph object registration (node/port/link) is complete. Documentation does not mention any failure.

@daschuer

Copy link
Copy Markdown
Member

Just did another test:

Issues:

  • Start with Pipewire
  • uncheck the Pipwire checkbox
  • Apply
    -> right channel is muted
  • Start with ALSA
  • Stlect Pipewire
  • Apply
  • Restart Mixxx
    -> Mixxx itself is selected as main output.

I think a reasonable choice would be "nothing" to have the "no Sounddervice configured" popup.
In the other PR, we will have the "default proxy device" for this, right?

@pri-yan-shu

Copy link
Copy Markdown
Contributor Author

Yes, currently it is using the mechanism of loading the first device, which doesn't make sense in PipeWire context. It is removed now. We will have a default device, and we will select only that (maybe lookup by name).

Start with Pipewire
uncheck the Pipwire checkbox
Apply
-> right channel is muted

I couldn't recreate it, but now in one of the changes, the checkbox is only queried at startup, and then cached, so any changes to it should not matter. Can you test again? Is only one link connected in the patchbay?

@daschuer daschuer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timer aka patchbay detection is not yet implemented, right?
This is OK for this PR. but than we should remove future artefacts of that feature.

The rest works and looks good now.

Comment thread src/soundio/pipewireenumerator.h Outdated
void registerOutput(const AudioOutput& output, AudioSource* src);

private:
void patchbayWaitTimer();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this function defined? I cannot find it.
Is this an artifact form a future PR?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its left from when I removed the timer. Timer can be added later, maybe in future there is no need for it (only patchbay sync mode).

@daschuer daschuer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you. Can you rebase the fixups? Than we can merge.

Detect any link creation to and from Mixxx node, and configure/unconfigure
input/output accordingly.
Because PipeWire device enumeration is async, the configuration loading
was happening before any devices were reported, which resulted in no
config device being opened at all. Now PipeWire manually triggers config
load after device enumeration is complete after initialization. For
simplicity, SoundManager::loadConfig is called everytime PipeWire is
initialized, effectively whenever "Query Devices" button is pressed.
If external patchbay connects Mixxx ports then skip loading config
…nfiguration

For cases when no Mixxx device is configured, like when using PipeWire patchbay mode
@daschuer

Copy link
Copy Markdown
Member

Thank you. Can we remove the draft state now?

@daschuer

Copy link
Copy Markdown
Member

The unit test fault is unrelated and tacked here: #16448

@pri-yan-shu
pri-yan-shu marked this pull request as ready for review August 15, 2026 06:39
@daschuer
daschuer merged commit b50c527 into mixxxdj:main Aug 16, 2026
32 of 33 checks passed
@daschuer

Copy link
Copy Markdown
Member

Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants