From 84010889129f0ebce39f37c6c705a688d7d3d1c1 Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Thu, 11 Jun 2026 10:09:25 +0530 Subject: [PATCH 01/13] rename SoundDeviceId.portAudioIndex to deviceIndex since PipeWire SoundDevices will be using this property portAudioIndex is inappropriate name --- src/soundio/sounddeviceportaudio.cpp | 8 ++++---- src/soundio/soundmanagerconfig.cpp | 18 +++++++++--------- src/soundio/soundmanagerutil.cpp | 5 +++-- src/soundio/soundmanagerutil.h | 14 +++++++------- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/soundio/sounddeviceportaudio.cpp b/src/soundio/sounddeviceportaudio.cpp index ae36f0190b05..5a9a8d0bed9a 100644 --- a/src/soundio/sounddeviceportaudio.cpp +++ b/src/soundio/sounddeviceportaudio.cpp @@ -129,7 +129,7 @@ SoundDevicePortAudio::SoundDevicePortAudio(UserSettingsPointer config, } else { m_deviceId.name = deviceInfo->name; } - m_deviceId.portAudioIndex = devIndex; + m_deviceId.deviceIndex = devIndex; m_strDisplayName = QString::fromUtf8(deviceInfo->name); m_numInputChannels = mixxx::audio::ChannelCount(m_deviceInfo->maxInputChannels); m_numOutputChannels = mixxx::audio::ChannelCount(m_deviceInfo->maxOutputChannels); @@ -248,7 +248,7 @@ SoundDeviceStatus SoundDevicePortAudio::open(bool isClkRefDevice, int syncBuffer << m_inputParams.channelCount; // Fill out the rest of the info. - m_outputParams.device = m_deviceId.portAudioIndex; + m_outputParams.device = m_deviceId.deviceIndex; m_outputParams.sampleFormat = paFloat32; m_outputParams.suggestedLatency = bufferMSec / 1000.0; #ifdef PA_USE_OBOE @@ -270,12 +270,12 @@ SoundDeviceStatus SoundDevicePortAudio::open(bool isClkRefDevice, int syncBuffer } #endif - m_inputParams.device = m_deviceId.portAudioIndex; + m_inputParams.device = m_deviceId.deviceIndex; m_inputParams.sampleFormat = paFloat32; m_inputParams.suggestedLatency = bufferMSec / 1000.0; m_inputParams.hostApiSpecificStreamInfo = nullptr; - qDebug() << "Opening stream with id" << m_deviceId.portAudioIndex; + qDebug() << "Opening stream with id" << m_deviceId.deviceIndex; m_lastCallbackEntrytoDacSecs = bufferMSec / 1000.0; diff --git a/src/soundio/soundmanagerconfig.cpp b/src/soundio/soundmanagerconfig.cpp index a13a987327cf..ea6786b3b6a8 100644 --- a/src/soundio/soundmanagerconfig.cpp +++ b/src/soundio/soundmanagerconfig.cpp @@ -28,7 +28,7 @@ const QString xmlAttributeDeckCount = "deck_count"; const QString xmlElementSoundDevice = "SoundDevice"; const QString xmlAttributeDeviceName = "name"; const QString xmlAttributeAlsaHwDevice = "alsaHwDevice"; -const QString xmlAttributePortAudioIndex = "portAudioIndex"; +const QString xmlAttributeDeviceIndex = "deviceIndex"; const QString xmlElementOutput = "output"; const QString xmlElementInput = "input"; @@ -104,10 +104,10 @@ bool SoundManagerConfig::readFromDisk() { if (match.hasMatch()) { deviceIdFromFile.name = match.captured(3); deviceIdFromFile.alsaHwDevice = match.captured(5); - deviceIdFromFile.portAudioIndex = match.captured(2).toInt(); + deviceIdFromFile.deviceIndex = match.captured(2).toInt(); } else { deviceIdFromFile.alsaHwDevice = devElement.attribute(xmlAttributeAlsaHwDevice); - deviceIdFromFile.portAudioIndex = devElement.attribute(xmlAttributePortAudioIndex).toInt(); + deviceIdFromFile.deviceIndex = devElement.attribute(xmlAttributeDeviceIndex).toInt(); } int devicesMatchingByName = 0; @@ -125,15 +125,15 @@ bool SoundManagerConfig::readFromDisk() { continue; } else if (devicesMatchingByName == 1) { // There is only one device with this name, so it is unambiguous - // which it is. Neither the alsaHwDevice nor portAudioIndex are + // which it is. Neither the alsaHwDevice nor deviceIndex are // very reliable as persistent identifiers across restarts of Mixxx. - // Set deviceIdFromFile's alsaHwDevice and portAudioIndex to match + // Set deviceIdFromFile's alsaHwDevice and deviceIndex to match // the hardwareDeviceId so operator== works for SoundDeviceId. for (const auto& soundDevice : soundDevices) { SoundDeviceId hardwareDeviceId = soundDevice->getDeviceId(); if (hardwareDeviceId.name == deviceIdFromFile.name) { deviceIdFromFile.alsaHwDevice = hardwareDeviceId.alsaHwDevice; - deviceIdFromFile.portAudioIndex = hardwareDeviceId.portAudioIndex; + deviceIdFromFile.deviceIndex = hardwareDeviceId.deviceIndex; } } } else { @@ -152,7 +152,7 @@ bool SoundManagerConfig::readFromDisk() { SoundDeviceId hardwareDeviceId = soundDevice->getDeviceId(); if (hardwareDeviceId.name == deviceIdFromFile.name && hardwareDeviceId.alsaHwDevice == deviceIdFromFile.alsaHwDevice) { - deviceIdFromFile.portAudioIndex = hardwareDeviceId.portAudioIndex; + deviceIdFromFile.deviceIndex = hardwareDeviceId.deviceIndex; break; } } @@ -165,7 +165,7 @@ bool SoundManagerConfig::readFromDisk() { outElements.count() && soundDevice->getNumInputChannels() >= inElements.count()) { - deviceIdFromFile.portAudioIndex = hardwareDeviceId.portAudioIndex; + deviceIdFromFile.deviceIndex = hardwareDeviceId.deviceIndex; break; } } @@ -236,7 +236,7 @@ bool SoundManagerConfig::writeToDisk() const { for (const auto& deviceId : deviceIds) { QDomElement devElement(doc.createElement(xmlElementSoundDevice)); devElement.setAttribute(xmlAttributeDeviceName, deviceId.name); - devElement.setAttribute(xmlAttributePortAudioIndex, deviceId.portAudioIndex); + devElement.setAttribute(xmlAttributeDeviceIndex, deviceId.deviceIndex); if (m_api == MIXXX_PORTAUDIO_ALSA_STRING) { devElement.setAttribute(xmlAttributeAlsaHwDevice, deviceId.alsaHwDevice); } diff --git a/src/soundio/soundmanagerutil.cpp b/src/soundio/soundmanagerutil.cpp index e2cd40bd6c70..964077a8e1eb 100644 --- a/src/soundio/soundmanagerutil.cpp +++ b/src/soundio/soundmanagerutil.cpp @@ -373,8 +373,9 @@ void AudioInput::setType(AudioPathType type) { QString SoundDeviceId::debugName() const { if (alsaHwDevice.isEmpty()) { - return name + QStringLiteral(", ") + QString::number(portAudioIndex); + return name + QStringLiteral(", ") + QString::number(deviceIndex); } else { - return name + QStringLiteral(", ") + alsaHwDevice + QStringLiteral(", ") + QString::number(portAudioIndex); + return name + QStringLiteral(", ") + alsaHwDevice + + QStringLiteral(", ") + QString::number(deviceIndex); } } diff --git a/src/soundio/soundmanagerutil.h b/src/soundio/soundmanagerutil.h index bf75bf659c60..b48c55d2db41 100644 --- a/src/soundio/soundmanagerutil.h +++ b/src/soundio/soundmanagerutil.h @@ -262,12 +262,13 @@ class SoundDeviceId final { /// The "hw:X,Y" device name. Remains an empty string if not using ALSA /// or using a non-hw ALSA device such as "default" or "pulse". QString alsaHwDevice; - int portAudioIndex; + int deviceIndex; QString debugName() const; SoundDeviceId() - : portAudioIndex(-1) {} + : deviceIndex(-1) { + } }; /// This must be registered with QMetaType::registerComparators for @@ -276,9 +277,8 @@ class SoundDeviceId final { inline bool operator==( const SoundDeviceId& lhs, const SoundDeviceId& rhs) { - return lhs.name == rhs.name - && lhs.alsaHwDevice == rhs.alsaHwDevice - && lhs.portAudioIndex == rhs.portAudioIndex; + return lhs.name == rhs.name && lhs.alsaHwDevice == rhs.alsaHwDevice && + lhs.deviceIndex == rhs.deviceIndex; } inline bool operator!=( @@ -290,7 +290,7 @@ inline bool operator!=( /// There is not really a use case for this, but it is required for QMetaType::registerComparators. inline bool operator<(const SoundDeviceId& lhs, const SoundDeviceId& rhs) { DEBUG_ASSERT(!"should never be invoked"); - return lhs.portAudioIndex < rhs.portAudioIndex; + return lhs.deviceIndex < rhs.deviceIndex; } Q_DECLARE_METATYPE(SoundDeviceId); @@ -300,7 +300,7 @@ inline qhash_seed_t qHash( qhash_seed_t seed = 0) { return qHash(id.name, seed) ^ qHash(id.alsaHwDevice, seed) ^ - qHash(id.portAudioIndex, seed); + qHash(id.deviceIndex, seed); } inline QDebug operator<<(QDebug dbg, const SoundDeviceId& soundDeviceId) { From a505458ffef602ed20a19e8391fc9147ba9b9c82 Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Thu, 11 Jun 2026 07:26:09 +0530 Subject: [PATCH 02/13] implement PipewireEnumerator and SoundDevicePipewire --- CMakeLists.txt | 14 + cmake/modules/FindPipeWire.cmake | 42 +++ src/soundio/networkenumerator.cpp | 6 +- src/soundio/networkenumerator.h | 4 +- src/soundio/pipewireenumerator.cpp | 414 ++++++++++++++++++++++++++++ src/soundio/pipewireenumerator.h | 168 +++++++++++ src/soundio/sounddevicepipewire.cpp | 154 +++++++++++ src/soundio/sounddevicepipewire.h | 61 ++++ src/soundio/soundmanager.cpp | 86 +++++- src/soundio/soundmanager.h | 21 +- tools/debian_buildenv.sh | 2 + tools/rpm_buildenv.sh | 1 + 12 files changed, 951 insertions(+), 22 deletions(-) create mode 100644 cmake/modules/FindPipeWire.cmake create mode 100644 src/soundio/pipewireenumerator.cpp create mode 100644 src/soundio/pipewireenumerator.h create mode 100644 src/soundio/sounddevicepipewire.cpp create mode 100644 src/soundio/sounddevicepipewire.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 97c6b7a14dd3..6945686eb67f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3741,6 +3741,20 @@ add_library( target_include_directories(mixxx-lib SYSTEM PUBLIC lib/portaudio) target_link_libraries(mixxx-lib PRIVATE PortAudioRingBuffer) +# PipeWire +default_option(PIPEWIRE "Enable the PipeWire backend" "UNIX AND NOT APPLE AND NOT ANDROID") +if(PIPEWIRE) + find_package(PipeWire REQUIRED) + target_link_libraries(mixxx-lib PUBLIC PipeWire::PipeWire) + target_compile_definitions(mixxx-lib PUBLIC __PIPEWIRE__) + target_sources( + mixxx-lib + PRIVATE + src/soundio/pipewireenumerator.cpp + src/soundio/sounddevicepipewire.cpp + ) +endif() + # PortMidi default_option(PORTMIDI "Enable the PortMidi backend for MIDI controllers" "NOT ANDROID") if(PORTMIDI) diff --git a/cmake/modules/FindPipeWire.cmake b/cmake/modules/FindPipeWire.cmake new file mode 100644 index 000000000000..b85f7a8c4a96 --- /dev/null +++ b/cmake/modules/FindPipeWire.cmake @@ -0,0 +1,42 @@ +#[=======================================================================[.rst: +FindPipeWire +-------- + +Finds the PipeWire library. + +Imported Targets +^^^^^^^^^^^^^^^^ + +This module provides the following imported targets, if found: + +``PipeWire::PipeWire`` + The PipeWire library + +Result Variables +^^^^^^^^^^^^^^^^ + +This will define the following variables: + +``PipeWire_FOUND`` + True if the system has the PipeWire library. + +#]=======================================================================] + +find_package(PkgConfig REQUIRED) +pkg_check_modules(PIPEWIRE REQUIRED IMPORTED_TARGET libpipewire-0.3) + +include(FindPackageHandleStandardArgs) + +find_package_handle_standard_args( + PipeWire + REQUIRED_VARS PIPEWIRE_FOUND + VERSION_VAR PIPEWIRE_VERSION +) + +if(PipeWire_FOUND AND NOT TARGET PipeWire::PipeWire) + add_library(PipeWire::PipeWire INTERFACE IMPORTED) + set_target_properties( + PipeWire::PipeWire + PROPERTIES INTERFACE_LINK_LIBRARIES PkgConfig::PIPEWIRE + ) +endif() diff --git a/src/soundio/networkenumerator.cpp b/src/soundio/networkenumerator.cpp index e3d2cf35b347..42639cd249ec 100644 --- a/src/soundio/networkenumerator.cpp +++ b/src/soundio/networkenumerator.cpp @@ -3,11 +3,11 @@ #include "engine/sidechain/enginenetworkstream.h" #include "soundio/sounddevice.h" -NetworkEnumerator::NetworkEnumerator(UserSettingsPointer config, - SoundManager* sm) +NetworkEnumerator::NetworkEnumerator(UserSettingsPointer pConfig, + SoundManager* pSoundManager) : m_pNetworkStream(QSharedPointer::create(2, 0)), m_pDevice(QSharedPointer::create( - config, sm, m_pNetworkStream)) { + pConfig, pSoundManager, m_pNetworkStream)) { } NetworkEnumerator::~NetworkEnumerator() { diff --git a/src/soundio/networkenumerator.h b/src/soundio/networkenumerator.h index ca21a12aa796..d5077effd56a 100644 --- a/src/soundio/networkenumerator.h +++ b/src/soundio/networkenumerator.h @@ -8,8 +8,8 @@ class NetworkEnumerator : public SoundDeviceEnumerator { public: - NetworkEnumerator(UserSettingsPointer config, - SoundManager* sm); + NetworkEnumerator(UserSettingsPointer pConfig, + SoundManager* pSoundManager); ~NetworkEnumerator() override; std::vector queryDevices() const override; diff --git a/src/soundio/pipewireenumerator.cpp b/src/soundio/pipewireenumerator.cpp new file mode 100644 index 000000000000..0163914d86c5 --- /dev/null +++ b/src/soundio/pipewireenumerator.cpp @@ -0,0 +1,414 @@ +#include "soundio/pipewireenumerator.h" + +#include +#include +#include +#include + +#include "moc_pipewireenumerator.cpp" +#include "soundio/sounddevice.h" +#include "soundio/sounddevicepipewire.h" +#include "soundio/soundmanager.h" +#include "util/assert.h" +#include "util/sample.h" +#include "util/trace.h" +#include "waveform/visualplayposition.h" + +namespace { + +const QString kAppGroup = QStringLiteral("[App]"); + +static const char* find_node_name(const struct spa_dict* props) { + static const char* const name_keys[] = { + PW_KEY_NODE_NAME, + PW_KEY_NODE_DESCRIPTION, + PW_KEY_APP_NAME, + PW_KEY_MEDIA_NAME, + }; + + for (const char* key : name_keys) { + const char* name = spa_dict_lookup(props, key); + if (name) { + return name; + } + } + return nullptr; +} + +} // namespace + +PipewireEnumerator::PipewireEnumerator(UserSettingsPointer, SoundManager* pManager) + : m_pSoundManager(pManager), + m_initialized(false), + m_audioLatencyUsage(kAppGroup, QStringLiteral("audio_latency_usage")) { + pw_init(nullptr, nullptr); + + m_pThreadLoop = pw_thread_loop_new("mixxx_loop", nullptr); + m_pContext = pw_context_new(pw_thread_loop_get_loop(m_pThreadLoop), nullptr, 0); + m_pCore = pw_context_connect(m_pContext, nullptr, 0); + m_pRegistry = pw_core_get_registry(m_pCore, PW_VERSION_REGISTRY, 0); + + // see https://docs.pipewire.org/page_man_pipewire-props_7.html + // and pipewire/keys.h header + m_pFilter = pw_filter_new(m_pCore, + "mixxx", + pw_properties_new(PW_KEY_MEDIA_NAME, + "Mixxx", + PW_KEY_MEDIA_TYPE, + "Audio", + PW_KEY_MEDIA_CATEGORY, + "Duplex", + PW_KEY_MEDIA_ROLE, + "Production", + PW_KEY_MEDIA_CLASS, + "Audio/Duplex", + PW_KEY_NODE_NAME, + "Mixxx", + PW_KEY_NODE_NICK, + "Mixxx", + nullptr)); + + spa_zero(m_registryListener); + spa_zero(m_metadataListener); + spa_zero(m_filterListener); + + pw_registry_add_listener(m_pRegistry, &m_registryListener, ®istry_events, this); + pw_filter_add_listener(m_pFilter, &m_filterListener, &filter_events, this); + + pw_filter_connect(m_pFilter, + PW_FILTER_FLAG_RT_PROCESS, + nullptr, + 0); + + pw_thread_loop_start(m_pThreadLoop); +} + +PipewireEnumerator::~PipewireEnumerator() { + pw_thread_loop_stop(m_pThreadLoop); + spa_hook_remove(&m_registryListener); + spa_hook_remove(&m_metadataListener); + pw_proxy_destroy((struct pw_proxy*)m_pRegistry); + pw_proxy_destroy((struct pw_proxy*)m_pMetadata); + pw_core_disconnect(m_pCore); + pw_context_destroy(m_pContext); + pw_thread_loop_destroy(m_pThreadLoop); + pw_deinit(); +} + +QList PipewireEnumerator::getSampleRates() const { + if (m_samplerates.empty()) { + return QList{ + mixxx::audio::SampleRate(44100), + mixxx::audio::SampleRate(48000), + mixxx::audio::SampleRate(96000), + }; + } + + return m_samplerates; +} + +void PipewireEnumerator::registryEventGlobal(uint32_t id, + uint32_t, + const char* pType, + uint32_t, + const struct spa_dict* pProps) { + if (strcmp(pType, PW_TYPE_INTERFACE_Metadata) == 0) { + const char* name = spa_dict_lookup(pProps, PW_KEY_METADATA_NAME); + if (strcmp(name, "settings") != 0) { + return; + } + + void* data = pw_registry_bind(m_pRegistry, + id, + PW_TYPE_INTERFACE_Metadata, + PW_VERSION_METADATA, + 0); + m_pMetadata = static_cast(data); + pw_metadata_add_listener(m_pMetadata, &m_metadataListener, &metadataEvents, this); + } else if (strcmp(pType, PW_TYPE_INTERFACE_Node) == 0) { + const char* media_class = spa_dict_lookup(pProps, PW_KEY_MEDIA_CLASS); + const char* media_type = spa_dict_lookup(pProps, PW_KEY_MEDIA_TYPE); + + bool isAudioNode = (media_class && strstr(media_class, "Audio")) || + (media_type && strstr(media_type, "Audio")); + + if (!isAudioNode) { + return; + } + + const char* name = find_node_name(pProps); + + m_objects.insert_or_assign(id, Object{Node{}}); + auto pDevice = QSharedPointer::create( + m_pConfig, m_pSoundManager, this, id, name); + m_pSoundManager->addDevice(pDevice); + m_soundDevices.insert_or_assign(id, std::move(pDevice)); + + if (strcmp(name, "Mixxx") == 0) { + m_filterId = id; + } + } else if (strcmp(pType, PW_TYPE_INTERFACE_Port) == 0) { + const uint32_t node_id = pw_properties_parse_int(spa_dict_lookup(pProps, PW_KEY_NODE_ID)); + const uint32_t port_id = pw_properties_parse_int(spa_dict_lookup(pProps, PW_KEY_PORT_ID)); + const char* dir = spa_dict_lookup(pProps, PW_KEY_PORT_DIRECTION); + const bool isInput = strcmp(dir, "in") == 0; + const spa_direction direction = isInput ? SPA_DIRECTION_INPUT : SPA_DIRECTION_OUTPUT; + + if (!m_soundDevices.contains(node_id)) { + // most likely midi or video node + return; + } + + m_objects.insert_or_assign(id, Object{Port(port_id, node_id, direction)}); + auto& soundDevice = m_soundDevices[node_id]; + soundDevice->registerDevicePort(id, pProps); + + if (node_id != m_filterId) { + return; + } + + for (auto& [deviceId, device] : m_openedDevices) { + if (isInput) { + for (auto& port : device.inputs) { + if (port.filterPort == port_id) { + auto devicePorts = m_soundDevices[deviceId]->getOutPorts(); + uint32_t devicePortId = devicePorts[port.devicePort].id; + createLink(deviceId, devicePortId, node_id, id); + } + } + } else { + for (auto& port : device.outputs) { + if (port_id == port.filterPort) { + auto devicePorts = m_soundDevices[deviceId]->getInPorts(); + uint32_t devicePortId = devicePorts[port.devicePort].id; + createLink(node_id, id, deviceId, devicePortId); + } + } + } + } + } else if (strcmp(pType, PW_TYPE_INTERFACE_Link) == 0) { + const uint32_t in_node = pw_properties_parse_int( + spa_dict_lookup(pProps, PW_KEY_LINK_INPUT_NODE)); + const uint32_t in_port = pw_properties_parse_int( + spa_dict_lookup(pProps, PW_KEY_LINK_INPUT_PORT)); + const uint32_t out_node = pw_properties_parse_int( + spa_dict_lookup(pProps, PW_KEY_LINK_OUTPUT_NODE)); + const uint32_t out_port = pw_properties_parse_int( + spa_dict_lookup(pProps, PW_KEY_LINK_OUTPUT_PORT)); + + if (in_node == m_filterId || out_node == m_filterId) { + m_objects.insert_or_assign(id, Object{Link(in_port, out_port)}); + } + } +} + +void PipewireEnumerator::registryEventGlobalRemove(unsigned int id) { + if (!m_objects.contains(id)) { + return; + } + + Object& object = m_objects.at(id); + + if (std::get_if(&object)) { + const auto& node = m_soundDevices.extract(id); + auto& device = node.mapped(); + if (device->isOpen()) { + device->close(); + } + m_pSoundManager->removeDevice(device); + } else if (auto* port = std::get_if(&object)) { + auto& device = m_soundDevices[port->nodeId]; + device->unregisterDevicePort(port->id, port->direction); + } + + m_objects.erase(id); +} + +std::vector PipewireEnumerator::queryDevices() const { + std::vector devices{}; + for (const auto& [id, device] : m_soundDevices) { + devices.push_back(device); + } + + return devices; +} + +void PipewireEnumerator::initialize() { +} + +int PipewireEnumerator::metadataProperty( + void* data, uint32_t, const char* key, const char*, const char* value) { + PipewireEnumerator* pEnumerator = static_cast(data); + + if (strcmp(key, "clock.allowed-rates") == 0) { + // parse json arrays like [ 44100, 48000, 96000 ] + QString s = value; + s.remove('['); + s.remove(']'); + + const QStringList parts = s.split(',', Qt::SkipEmptyParts); + + for (const QString& part : parts) { + pEnumerator->m_samplerates.push_back(mixxx::audio::SampleRate(part.trimmed().toInt())); + } + pEnumerator->m_pSoundManager->checkConfig(); + } + return 0; +} + +bool PipewireEnumerator::isOpen(uint32_t id) { + return m_openedDevices.contains(id); +} + +void PipewireEnumerator::openDevice( + uint32_t id, const std::set& inChans, const std::set& outChans) { + VERIFY_OR_DEBUG_ASSERT(!m_openedDevices.contains(id)) { + qWarning() << "device:" << id << "already open"; + return; + } + + pw_thread_loop_lock(m_pThreadLoop); + + size_t numInPorts = 0; + size_t numOutPorts = 0; + + for (auto& [id, device] : m_openedDevices) { + numInPorts += device.inputs.size(); + numOutPorts += device.outputs.size(); + } + + // these correspond to the AudioInputs and filter inputs + // and device outputs + std::vector inputs; + for (uint8_t i : inChans) { + size_t filterPortIndex = inputs.size() + numInPorts; + pw_properties* props = pw_properties_new( + // see pipewire/keys.h header + PW_KEY_FORMAT_DSP, + "32 bit float mono audio", + nullptr); + pw_properties_setf(props, PW_KEY_PORT_NAME, "in:%zu", filterPortIndex); + void* port_data = pw_filter_add_port(m_pFilter, + SPA_DIRECTION_INPUT, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + 0, + props, + nullptr, + 0); + inputs.emplace_back(port_data, i, filterPortIndex); + } + // these correspond to the AudioInputs and filter outputs + // and device inputs + std::vector outputs; + for (uint8_t i : outChans) { + size_t filterPortIndex = outputs.size() + numOutPorts; + pw_properties* props = pw_properties_new( + PW_KEY_FORMAT_DSP, "32 bit float mono audio", nullptr); + pw_properties_setf(props, PW_KEY_PORT_NAME, "out:%zu", filterPortIndex); + void* port_data = pw_filter_add_port(m_pFilter, + SPA_DIRECTION_OUTPUT, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + 0, + props, + nullptr, + 0); + outputs.emplace_back(port_data, i, filterPortIndex); + } + pw_thread_loop_unlock(m_pThreadLoop); + + m_openedDevices.emplace(id, Device{std::move(inputs), std::move(outputs)}); +} + +void PipewireEnumerator::closeDevice(uint32_t id) { + VERIFY_OR_DEBUG_ASSERT(m_openedDevices.contains(id)) { + qWarning() << "device:" << id << "not opened"; + return; + } + + auto& device = m_openedDevices[id]; + + pw_thread_loop_lock(m_pThreadLoop); + for (auto& port : device.inputs) { + pw_filter_remove_port(port.pPortData); + } + + for (auto& port : device.outputs) { + pw_filter_remove_port(port.pPortData); + } + pw_thread_loop_unlock(m_pThreadLoop); + + m_openedDevices.erase(id); +} + +void PipewireEnumerator::callback(const spa_io_position* pos) { + // This must be the very first call, else timeInfo becomes invalid + m_clkRefTimer.restart().toDoubleSeconds(); + VisualPlayPosition::setCallbackEntryToDacSecs( + pos->clock.delay / pos->clock.rate.denom, m_clkRefTimer); + + Trace trace("SoundDevicePw::callbackProcessClkRef"); + +#if PW_CHECK_VERSION(0, 3, 50) + if (pos->clock.xrun > xrun_duration) { + xrun_duration = pos->clock.xrun; + m_pSoundManager->underflowHappened(6); + } +#endif + + const uint64_t framesPerBuffer = pos->clock.duration; + m_pSoundManager->processUnderflowHappened(framesPerBuffer); + + for (auto& [id, device] : m_openedDevices) { + auto soundDevice = m_soundDevices[id]; + auto& ports = device.inputs; + for (const auto& port : ports) { + void* buffer = pw_filter_get_dsp_buffer(port.pPortData, framesPerBuffer); + soundDevice->writeInput(static_cast(buffer), port.devicePort, framesPerBuffer); + } + m_pSoundManager->pushInputBuffers(soundDevice->inputs(), framesPerBuffer); + } + + m_pSoundManager->onDeviceOutputCallback(framesPerBuffer); + + for (auto& [id, device] : m_openedDevices) { + auto& soundDevice = m_soundDevices[id]; + auto& ports = device.outputs; + for (const auto& port : ports) { + void* buffer = pw_filter_get_dsp_buffer(port.pPortData, framesPerBuffer); + if (!buffer) { + continue; + } + SampleUtil::clear(static_cast(buffer), framesPerBuffer); + soundDevice->writeOutput(static_cast(buffer), port.devicePort, framesPerBuffer); + } + } +} + +void PipewireEnumerator::createLink(uint32_t outNodeId, + uint32_t outPortId, + uint32_t inNodeId, + uint32_t inPortId) { + spa_dict_item items[6]; + spa_dict props = SPA_DICT_INIT(items, 0); + + std::string strOutNode = std::to_string(outNodeId); + std::string strOutPort = std::to_string(outPortId); + std::string strInNode = std::to_string(inNodeId); + std::string strInPort = std::to_string(inPortId); + + items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_OUTPUT_NODE, strOutNode.c_str()); + items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_OUTPUT_PORT, strOutPort.c_str()); + items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_INPUT_NODE, strInNode.c_str()); + items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_INPUT_PORT, strInPort.c_str()); + items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_LINGER, "true"); + + struct pw_proxy* proxy = (struct pw_proxy*)pw_core_create_object(m_pCore, + "link-factory", + PW_TYPE_INTERFACE_Link, + PW_VERSION_LINK, + &props, + 0); + if (proxy) { + pw_proxy_destroy(proxy); + } +} diff --git a/src/soundio/pipewireenumerator.h b/src/soundio/pipewireenumerator.h new file mode 100644 index 000000000000..068fca33c685 --- /dev/null +++ b/src/soundio/pipewireenumerator.h @@ -0,0 +1,168 @@ +#pragma once + +#include +#include +#include + +#include + +#include "preferences/usersettings.h" +#include "soundio/sounddevice.h" +#include "soundio/sounddeviceenumerator.h" +#include "soundio/sounddevicepipewire.h" +#include "soundio/soundmanager.h" + +class PipewireEnumerator : public SoundDeviceEnumerator { + Q_OBJECT + public: + PipewireEnumerator(UserSettingsPointer pConfig, + SoundManager* pManager); + ~PipewireEnumerator() override; + + QList getSampleRates() const override; + std::vector queryDevices() const override; + std::vector getAPIs() const override { + return std::vector{"PipeWire"}; + } + + void initialize(); + + pw_core* getCore(); + pw_thread_loop* getThreadLoop(); + + bool isOpen(uint32_t id); + void openDevice(uint32_t id, std::set& inChannels, std::set& outChannels); + void closeDevice(uint32_t id); + + private: + static void registryEventGlobalOuter(void* data, + uint32_t id, + uint32_t permissions, + const char* type, + uint32_t version, + const struct spa_dict* props) { + ((PipewireEnumerator*)data)->registryEventGlobal(id, permissions, type, version, props); + } + + static void registryEventGlobalRemoveOuter(void* data, uint32_t id) { + ((PipewireEnumerator*)data)->registryEventGlobalRemove(id); + } + + static constexpr pw_registry_events registry_events = { + .version = PW_VERSION_REGISTRY_EVENTS, + .global = registryEventGlobalOuter, + .global_remove = registryEventGlobalRemoveOuter, + }; + + static int metadataProperty(void* data, + uint32_t id, + const char* key, + const char* type, + const char* value); + + static constexpr struct pw_metadata_events metadataEvents = { + .version = PW_VERSION_METADATA_EVENTS, + .property = metadataProperty}; + + static void callback(void* data, spa_io_position* pos) { + ((PipewireEnumerator*)data)->callback(pos); + } + + static constexpr pw_filter_events filter_events{ + .version = PW_VERSION_FILTER_EVENTS, + .destroy = nullptr, + .state_changed = nullptr, + .io_changed = nullptr, + .param_changed = nullptr, + .add_buffer = nullptr, + .remove_buffer = nullptr, + .process = callback, + .drained = nullptr, + .command = nullptr, + }; + + void registryEventGlobal(uint32_t id, + uint32_t permissions, + const char* type, + uint32_t version, + const struct spa_dict* props); + void registryEventGlobalRemove(unsigned int id); + + void callback(const spa_io_position* pos); + + void addDevice(uint32_t id); + void removeDevice(uint32_t id); + + void writeInput(const float* input, int channel, int framesPerBuffer, int offset = 0); + void writeOutput(float* output, int channel, int framesPerBuffer, int offset = 0); + + void createLink(uint32_t outNodeId, + uint32_t outPortId, + uint32_t inNodeI, + uint32_t inPortId); + std::unordered_map> m_soundDevices; + + struct Link { + uint32_t input; + uint32_t output; + }; + + struct Port { + // this is not global id, but port.id, which starts from 0 + // using this we can directly index into the port vector + // on the node from nodeId + // global id indexes into maps, port.id indexes into vectors + uint32_t id; + uint32_t nodeId; + spa_direction direction; + }; + + struct Node {}; + using Object = std::variant; + + std::unordered_map m_objects; + QList m_samplerates; + + SoundManager* m_pSoundManager; + UserSettingsPointer m_pConfig; + + pw_core* m_pCore; + pw_registry* m_pRegistry; + pw_context* m_pContext; + pw_metadata* m_pMetadata; + pw_thread_loop* m_pThreadLoop; + spa_hook m_registryListener; + spa_hook m_metadataListener; + spa_hook m_filterListener; + pw_filter* m_pFilter; + + struct Device { + struct Port { + void* pPortData; + + // these are our own indices, and are not assigned by + // pipewire in any way + uint32_t devicePort; + uint32_t filterPort; + }; + + // inputs correspond to filter inputs and soundDevice outputs + std::vector inputs; + // outputs correspond to filter outputs and soundDevice inputs + std::vector outputs; + }; + + std::unordered_map m_openedDevices; + + bool m_initialized; + uint64_t xrun_duration; + int m_invalidTimeInfoCount; + double m_lastCallbackEntrytoDacSecs; + PerformanceTimer m_clkRefTimer; + mixxx::audio::SampleRate m_sampleRate; + + PollingControlProxy m_audioLatencyUsage; + mixxx::Duration m_timeInAudioCallback; + int m_framesSinceAudioLatencyUsageUpdate; + uint32_t m_filterId; +}; diff --git a/src/soundio/sounddevicepipewire.cpp b/src/soundio/sounddevicepipewire.cpp new file mode 100644 index 000000000000..9498faabb986 --- /dev/null +++ b/src/soundio/sounddevicepipewire.cpp @@ -0,0 +1,154 @@ +#include "sounddevicepipewire.h" + +#include + +#include "soundio/pipewireenumerator.h" +#include "soundio/sounddevice.h" +#include "util/sample.h" + +SoundDevicePipewire::SoundDevicePipewire(UserSettingsPointer pConfig, + SoundManager* pManager, + PipewireEnumerator* pEnumerator, + uint32_t id, + const std::string_view name) + : SoundDevice(pConfig, pManager), + m_pEnumerator(pEnumerator) { + m_hostAPI = "PipeWire"; + m_deviceId.name = name.data(); + m_deviceId.deviceIndex = id; + m_strDisplayName = QString::fromUtf8(name); + m_numInputChannels = mixxx::audio::ChannelCount(0); + m_numOutputChannels = mixxx::audio::ChannelCount(0); +} + +SoundDeviceStatus SoundDevicePipewire::open(bool, int) { + std::set inChans; + for (auto& input : m_audioInputs) { + ChannelGroup channelGroup = input.getChannelGroup(); + uint8_t highChannel = channelGroup.getChannelBase() + channelGroup.getChannelCount(); + for (uint8_t i = channelGroup.getChannelBase(); i < highChannel; i++) { + inChans.insert(i); + } + } + + std::set outChans; + for (auto& output : m_audioOutputs) { + ChannelGroup channelGroup = output.getChannelGroup(); + uint8_t highChannel = channelGroup.getChannelBase() + channelGroup.getChannelCount(); + for (uint8_t i = channelGroup.getChannelBase(); i < highChannel; i++) { + outChans.insert(i); + } + } + + m_pEnumerator->openDevice(m_deviceId.deviceIndex, inChans, outChans); + return SoundDeviceStatus::Ok; +} + +bool SoundDevicePipewire::isOpen() const { + return m_pEnumerator->isOpen(m_deviceId.deviceIndex); +} + +SoundDeviceStatus SoundDevicePipewire::close() { + m_pEnumerator->closeDevice(m_deviceId.deviceIndex); + m_inPorts.clear(); + m_outPorts.clear(); + return SoundDeviceStatus::Ok; +} + +void SoundDevicePipewire::writeOutput(float* output, int channel, int framesPerBuffer, int offset) { + for (const auto& out : std::as_const(m_audioOutputs)) { + ChannelGroup chanGroup = out.getChannelGroup(); + const int iChannelCount = chanGroup.getChannelCount(); + const int iChannelBase = chanGroup.getChannelBase(); + const int iChannelEnd = iChannelCount + iChannelBase; + + if (channel < iChannelBase || channel > iChannelEnd) { + continue; + } + + const CSAMPLE* pOutputBuffer = &out.getBuffer()[offset]; + + if (iChannelCount == 1) { + for (int i = 0; i < framesPerBuffer; i++) { + output[i] = pOutputBuffer[i * 2]; + } + } else { + for (int i = 0; i < framesPerBuffer; i++) { + output[i] = pOutputBuffer[i * iChannelCount + channel]; + } + } + } +} + +void SoundDevicePipewire::writeInput( + const float* input, int channel, int framesPerBuffer, int offset) { + for (const auto& in : std::as_const(m_audioInputs)) { + ChannelGroup chanGroup = in.getChannelGroup(); + const int iChannelCount = chanGroup.getChannelCount(); + const int iChannelBase = chanGroup.getChannelBase(); + const int iChannelEnd = iChannelCount + iChannelBase; + + if (channel < iChannelBase || channel > iChannelEnd) { + continue; + } + + CSAMPLE* pInputBuffer = &in.getBuffer()[offset]; + + if (iChannelCount == 1) { + if (input) { + for (int i = 0; i < framesPerBuffer; i++) { + pInputBuffer[i] = input[i]; + pInputBuffer[i + 1] = input[i]; + } + } else { + SampleUtil::fill(pInputBuffer, 0, framesPerBuffer * 2); + } + } else { + if (input) { + for (int i = 0; i < framesPerBuffer; i++) { + pInputBuffer[i * iChannelCount + channel] = input[i]; + } + } else { + for (int i = 0; i < framesPerBuffer; i++) { + pInputBuffer[i * iChannelCount + channel] = 0; + } + } + } + } +} + +void SoundDevicePipewire::registerDevicePort(uint32_t id, const struct spa_dict* props) { + std::string name = spa_dict_lookup(props, PW_KEY_PORT_ALIAS); + const char* dir = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION); + + if (name.empty()) { + name = spa_dict_lookup(props, PW_KEY_PORT_NAME); + } + + if (name.empty()) { + name = dir; + name += ":"; + name += spa_dict_lookup(props, PW_KEY_PORT_ID); + } + + if (strcmp(dir, "in") == 0) { + m_inPorts.emplace_back(id, name); + m_numOutputChannels = mixxx::audio::ChannelCount::fromInt(m_inPorts.size()); + } else if (strcmp(dir, "out") == 0) { + m_outPorts.emplace_back(id, name); + m_numInputChannels = mixxx::audio::ChannelCount::fromInt(m_outPorts.size()); + } +} + +void SoundDevicePipewire::unregisterDevicePort(uint32_t id, spa_direction direction) { + switch (direction) { + case SPA_DIRECTION_INPUT: + m_inPorts.erase(m_inPorts.begin() + id); + m_numInputChannels = mixxx::audio::ChannelCount::fromInt(m_outPorts.size()); + break; + case SPA_DIRECTION_OUTPUT: + m_outPorts.erase(m_outPorts.begin() + id); + m_numOutputChannels = mixxx::audio::ChannelCount::fromInt(m_inPorts.size()); + break; + } +} diff --git a/src/soundio/sounddevicepipewire.h b/src/soundio/sounddevicepipewire.h new file mode 100644 index 000000000000..721be27a344a --- /dev/null +++ b/src/soundio/sounddevicepipewire.h @@ -0,0 +1,61 @@ +#pragma once + +#include + +#include "sounddevice.h" +#include "soundio/soundmanagerconfig.h" + +class SoundManager; +class PipewireEnumerator; + +class SoundDevicePipewire : public SoundDevice { + public: + SoundDevicePipewire(UserSettingsPointer pConfig, + SoundManager* pManager, + PipewireEnumerator* pEnumerator, + uint32_t id, + const std::string_view name); + SoundDeviceStatus open(bool isClkRefDevice, int syncBuffers) override; + bool isOpen() const override; + SoundDeviceStatus close() override; + + void readProcess(SINT) override { + } + void writeProcess(SINT) override { + } + QString getError() const override { + return {}; + } + + mixxx::audio::SampleRate getDefaultSampleRate() const override { + return SoundManagerConfig::kMixxxDefaultSampleRate; + } + + void writeOutput(float* output, int channel, int framesPerBuffer, int offset = 0); + void writeInput(const float* input, int channel, int framesPerBuffer, int offset = 0); + + void createLink(uint32_t outNodeId, + uint32_t outPortId, + uint32_t inNodeId, + uint32_t inPortId); + void registerDevicePort(uint32_t id, const struct spa_dict* props); + void unregisterDevicePort(uint32_t id, spa_direction direction); + + struct Port { + uint32_t id; + std::string name; + }; + + std::span getInPorts() const { + return m_inPorts; + } + + std::span getOutPorts() const { + return m_outPorts; + } + + private: + PipewireEnumerator* m_pEnumerator; + std::vector m_inPorts; + std::vector m_outPorts; +}; diff --git a/src/soundio/soundmanager.cpp b/src/soundio/soundmanager.cpp index 4909393bb7b8..4fcf17848fd9 100644 --- a/src/soundio/soundmanager.cpp +++ b/src/soundio/soundmanager.cpp @@ -11,9 +11,11 @@ #include "soundio/networkenumerator.h" #include "soundio/portaudioenumerator.h" #include "soundio/sounddevice.h" +#include "soundio/sounddeviceenumerator.h" #include "soundio/sounddevicenetwork.h" #include "soundio/sounddevicenotfound.h" #include "soundio/sounddeviceportaudio.h" +#include "soundio/soundmanagerconfig.h" #include "soundio/soundmanagerutil.h" #include "util/cmdlineargs.h" #include "util/compatibility/qatomic.h" @@ -21,6 +23,10 @@ #include "util/sample.h" #include "vinylcontrol/defs_vinylcontrol.h" +#ifdef __PIPEWIRE__ +#include "soundio/pipewireenumerator.h" +#endif + namespace { const QString kAppGroup = QStringLiteral("[App]"); @@ -48,8 +54,11 @@ SoundManager::SoundManager(UserSettingsPointer pConfig, m_underflowUpdateCount(0), m_audioLatencyOverloadCount(kAppGroup, QStringLiteral("audio_latency_overload_count")), m_audioLatencyOverload(kAppGroup, QStringLiteral("audio_latency_overload")), - m_paEnumerator(pConfig, this), - m_networkEnumerator(pConfig, this) { + m_paEnumerator(std::make_unique(pConfig, this)), +#ifdef __PIPEWIRE__ + m_pipewireEnumerator(std::make_unique(pConfig, this)), +#endif + m_networkEnumerator(std::make_unique(pConfig, this)) { // TODO(xxx) some of these ControlObject are not needed by soundmanager, or are unused here. // It is possible to take them out? m_pControlObjectSoundStatusCO = new ControlObject( @@ -95,7 +104,7 @@ QList SoundManager::getDeviceList( // input/output. QList filteredDeviceList; - for (const auto& pDevice : m_paEnumerator.queryDevices()) { + for (const auto& pDevice : m_devices) { // Skip devices that don't match the API, don't have input channels when // we want input devices, or don't have output channels when we want // output devices. If searching for both input and output devices, @@ -103,7 +112,8 @@ QList SoundManager::getDeviceList( const bool hasOutputs = pDevice->getNumOutputChannels().isValid(); const bool hasInputs = pDevice->getNumInputChannels().isValid(); qDebug() << "SoundManager::getDeviceList" << pDevice->getHostAPI() - << filterAPI << pDevice->getNumOutputChannels() + << pDevice->getDeviceId().debugName() << filterAPI + << pDevice->getNumOutputChannels() << pDevice->getNumInputChannels(); if (pDevice->getHostAPI() != filterAPI || (bOutputDevices && !bInputDevices && !hasOutputs) || @@ -111,6 +121,7 @@ QList SoundManager::getDeviceList( (!hasInputs && !hasOutputs)) { continue; } + filteredDeviceList.push_back(pDevice); } @@ -120,10 +131,16 @@ QList SoundManager::getDeviceList( QList SoundManager::getHostAPIList() const { QList apiList; - for (const auto& api : m_paEnumerator.getAPIs()) { + for (const auto& api : m_paEnumerator->getAPIs()) { apiList.push_back(api.c_str()); } +#ifdef __PIPEWIRE__ + for (const auto& api : m_pipewireEnumerator->getAPIs()) { + apiList.push_back(api.c_str()); + } +#endif + return apiList; } @@ -215,16 +232,22 @@ void SoundManager::clearDeviceList(bool sleepAfterClosing) { m_devices.clear(); m_pErrorDevice.clear(); - m_paEnumerator.terminate(); + m_paEnumerator->terminate(); } QList SoundManager::getSampleRates(const QString& api) const { if (api == MIXXX_PORTAUDIO_JACK_STRING) { // queryDevices must have been called for this to work, but the // ctor calls it -bkgood - return m_paEnumerator.getJackSampleRates(); - } else if (!api.isEmpty()) { - return m_paEnumerator.getSampleRates(); + return m_paEnumerator->getJackSampleRates(); + } +#ifdef __PIPEWIRE__ + else if (api == MIXXX_PIPEWIRE_STRING) { + return m_pipewireEnumerator->getSampleRates(); + } +#endif + else if (!api.isEmpty()) { + return m_paEnumerator->getSampleRates(); } return QList{ mixxx::audio::SampleRate(44100), @@ -240,14 +263,26 @@ QList SoundManager::getSampleRates() const { void SoundManager::queryDevices() { qDebug() << "SoundManager::queryDevices()"; - m_paEnumerator.initialize(); + m_devices.clear(); + m_paEnumerator->initialize(); - for (auto& device : m_paEnumerator.queryDevices()) { - m_devices.push_back(SoundDevicePointer(device)); + for (auto& device : m_paEnumerator->queryDevices()) { + m_devices.push_back(device); + qDebug() << "m_devices.push_back " << device->getDisplayName(); } - for (auto& device : m_networkEnumerator.queryDevices()) { - m_devices.push_back(SoundDevicePointer(device)); +#ifdef __PIPEWIRE__ + for (auto& device : m_pipewireEnumerator->queryDevices()) { + m_devices.push_back(device); + qDebug() << "m_devices.push_back " << device->getDisplayName(); + } + + m_pipewireEnumerator->initialize(); +#endif + + for (auto& device : m_networkEnumerator->queryDevices()) { + m_devices.push_back(device); + qDebug() << "m_devices.push_back " << device->getDisplayName(); } // now tell the prefs that we updated the device list -- bkgood @@ -623,3 +658,26 @@ void SoundManager::processUnderflowHappened(SINT framesPerBuffer) { --m_underflowUpdateCount; } } + +void SoundManager::addDevice(SoundDevicePointer pDevice) { + m_devices.push_back(pDevice); + qDebug() << "SoundManager::addDevice" << pDevice->getDisplayName(); + emit deviceAdded(pDevice); +} + +// device closing is handled before this +void SoundManager::removeDevice(SoundDevicePointer pDevice) { + for (const auto& device : std::as_const(m_devices)) { + if (device == pDevice) { + qDebug() << "SoundManager::removeDevice" << pDevice->getDisplayName(); + m_devices.removeOne(pDevice); + emit deviceRemoved(pDevice); + return; + } + } +} + +// device port/link changed +void SoundManager::updateDevice(SoundDevicePointer pDevice) { + emit deviceUpdated(pDevice); +} diff --git a/src/soundio/soundmanager.h b/src/soundio/soundmanager.h index 1138e7cffa08..a468cd285739 100644 --- a/src/soundio/soundmanager.h +++ b/src/soundio/soundmanager.h @@ -5,6 +5,7 @@ #include #include #include +#include #include "audio/types.h" #include "control/pollingcontrolproxy.h" @@ -19,6 +20,7 @@ class EngineMixer; class ControlObject; +class PipewireEnumerator; #define SOUNDMANAGER_DISCONNECTED 0 #define SOUNDMANAGER_CONNECTING 1 @@ -87,7 +89,7 @@ class SoundManager : public QObject { QList registeredInputs() const; QSharedPointer getNetworkStream() const { - return m_networkEnumerator.getNetworkStream(); + return m_networkEnumerator->getNetworkStream(); } void underflowHappened(int code) { @@ -115,6 +117,14 @@ class SoundManager : public QObject { void devicesClosed(); // emitted when the sound devices have been closed and resources freed void outputRegistered(const AudioOutput& output, AudioSource* src); void inputRegistered(const AudioInput& input, AudioDestination* dest); + void deviceAdded(SoundDevicePointer device); + void deviceRemoved(SoundDevicePointer device); + void deviceUpdated(SoundDevicePointer device); + + public slots: + void addDevice(SoundDevicePointer device); + void removeDevice(SoundDevicePointer device); + void updateDevice(SoundDevicePointer device); private slots: void completeDevicesClosing(); @@ -150,6 +160,11 @@ class SoundManager : public QObject { PollingControlProxy m_audioLatencyOverloadCount; PollingControlProxy m_audioLatencyOverload; - PortAudioEnumerator m_paEnumerator; - NetworkEnumerator m_networkEnumerator; + std::unique_ptr m_paEnumerator; + +#ifdef __PIPEWIRE__ + std::unique_ptr m_pipewireEnumerator; +#endif + + std::unique_ptr m_networkEnumerator; }; diff --git a/tools/debian_buildenv.sh b/tools/debian_buildenv.sh index 665a1a37adbe..c9c833320866 100755 --- a/tools/debian_buildenv.sh +++ b/tools/debian_buildenv.sh @@ -114,6 +114,7 @@ case "$1" in libmsgsl-dev \ libopus-dev \ libopusfile-dev \ + libpipewire-0.3-dev \ libportmidi-dev \ libprotobuf-dev \ libqt6opengl6-dev \ @@ -123,6 +124,7 @@ case "$1" in libshout-idjc-dev \ libsndfile1-dev \ libsoundtouch-dev \ + libspa-0.2-dev \ libsqlite3-dev \ libssl-dev \ libtag1-dev \ diff --git a/tools/rpm_buildenv.sh b/tools/rpm_buildenv.sh index 828c6721a315..0c216974040a 100755 --- a/tools/rpm_buildenv.sh +++ b/tools/rpm_buildenv.sh @@ -47,6 +47,7 @@ case "$1" in libmad-devel \ libmodplug-devel \ libmp4v2-devel \ + pipewire0.2-devel \ libsndfile-devel \ libusb1-devel \ libvorbis-devel \ From 7f767bdad6ee9ce9b996a394feadd7a73b4587e6 Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Tue, 16 Jun 2026 22:34:14 +0530 Subject: [PATCH 03/13] Implement PipeWire hotplug Hotplug is for node/port creation/destruction. Since how pipewire objects show up on the registry, when a SoundDevice is created and the preference page is signalled, it has no ports, so it is not added to DlgPrefSound or any DlgPrefSoundItem (although currently the code is signalling on node creation anyway). On subsequent port registration is the SoundDevice added to the page. --- src/preferences/dialog/dlgprefsound.cpp | 83 +++++++++++++++++++++ src/preferences/dialog/dlgprefsound.h | 11 ++- src/preferences/dialog/dlgprefsounditem.cpp | 72 ++++++++++++++++++ src/preferences/dialog/dlgprefsounditem.h | 6 +- src/soundio/pipewireenumerator.cpp | 25 ++++++- src/soundio/pipewireenumerator.h | 7 ++ src/soundio/soundmanager.cpp | 10 +-- src/soundio/soundmanager.h | 21 ++++-- 8 files changed, 215 insertions(+), 20 deletions(-) diff --git a/src/preferences/dialog/dlgprefsound.cpp b/src/preferences/dialog/dlgprefsound.cpp index 316a1ebae989..3ce9ecabcaaf 100644 --- a/src/preferences/dialog/dlgprefsound.cpp +++ b/src/preferences/dialog/dlgprefsound.cpp @@ -6,12 +6,15 @@ #include #include "control/controlproxy.h" +#include "defs_urls.h" #include "engine/enginebuffer.h" #include "engine/enginemixer.h" #include "mixer/playermanager.h" #include "moc_dlgprefsound.cpp" #include "preferences/dialog/dlgprefsounditem.h" +#include "soundio/sounddevice.h" #include "soundio/soundmanager.h" +#include "soundio/soundmanagerutil.h" #include "util/rlimit.h" #include "util/scopedoverridecursor.h" @@ -94,6 +97,21 @@ DlgPrefSound::DlgPrefSound(QWidget* pParent, this, &DlgPrefSound::refreshDevices); + connect(m_pSoundManager.get(), + &SoundManager::deviceAdded, + this, + &DlgPrefSound::addDevice); + + connect(m_pSoundManager.get(), + &SoundManager::deviceRemoved, + this, + &DlgPrefSound::removeDevice); + + connect(m_pSoundManager.get(), + &SoundManager::deviceChannelsUpdated, + this, + &DlgPrefSound::updateDeviceChannels); + apiComboBox->clear(); apiComboBox->addItem(SoundManagerConfig::kEmptyComboBox, SoundManagerConfig::kDefaultAPI); @@ -508,14 +526,23 @@ void DlgPrefSound::connectSoundItem(DlgPrefSoundItem* pItem) { connect(this, &DlgPrefSound::writePaths, pItem, &DlgPrefSoundItem::writePath); if (pItem->isInput()) { connect(this, &DlgPrefSound::refreshInputDevices, pItem, &DlgPrefSoundItem::refreshDevices); + connect(this, &DlgPrefSound::addInputDevice, pItem, &DlgPrefSoundItem::addDevice); + connect(this, &DlgPrefSound::removeInputDevice, pItem, &DlgPrefSoundItem::removeDevice); } else { connect(this, &DlgPrefSound::refreshOutputDevices, pItem, &DlgPrefSoundItem::refreshDevices); + connect(this, &DlgPrefSound::addOutputDevice, pItem, &DlgPrefSoundItem::addDevice); + connect(this, &DlgPrefSound::removeOutputDevice, pItem, &DlgPrefSoundItem::removeDevice); } connect(this, &DlgPrefSound::updatingAPI, pItem, &DlgPrefSoundItem::save); connect(this, &DlgPrefSound::updatedAPI, pItem, &DlgPrefSoundItem::reload); + connect(this, + &DlgPrefSound::deviceChannelsUpdated, + pItem, + &DlgPrefSoundItem::updateDeviceChannels); + connect(this, &DlgPrefSound::deviceRouteUpdated, pItem, &DlgPrefSoundItem::updateDeviceRoute); } void DlgPrefSound::insertItem(DlgPrefSoundItem *pItem, QVBoxLayout *pLayout) { @@ -797,6 +824,62 @@ void DlgPrefSound::refreshDevices() { emit refreshInputDevices(m_inputDevices); } +void DlgPrefSound::addDevice(SoundDevicePointer pDevice) { + const bool hasInputs = pDevice->getNumInputChannels().isValid(); + const bool hasOutputs = pDevice->getNumOutputChannels().isValid(); + + if (hasInputs) { + m_inputDevices.append(pDevice); + emit addInputDevice(pDevice); + } + if (hasOutputs) { + m_outputDevices.append(pDevice); + emit addOutputDevice(pDevice); + } +} + +void DlgPrefSound::removeDevice(SoundDevicePointer pDevice) { + const bool hasInputs = pDevice->getNumInputChannels().isValid(); + const bool hasOutputs = pDevice->getNumOutputChannels().isValid(); + + if (hasInputs && m_inputDevices.removeOne(pDevice)) { + emit removeInputDevice(pDevice); + } + + if (hasOutputs && m_outputDevices.removeOne(pDevice)) { + emit removeOutputDevice(pDevice); + } +} + +void DlgPrefSound::updateDeviceChannels(SoundDevicePointer pDevice) { + const bool hasInputs = pDevice->getNumInputChannels().isValid(); + const bool hasOutputs = pDevice->getNumOutputChannels().isValid(); + const bool hadInputs = m_inputDevices.contains(pDevice); + const bool hadOutputs = m_outputDevices.contains(pDevice); + const bool listsModified = (hasInputs ^ hadInputs) || (hasOutputs ^ hadOutputs); + + if (!listsModified) { + emit deviceChannelsUpdated(pDevice); + return; + } + + if (hadInputs && !hasInputs) { + m_inputDevices.removeOne(pDevice); + emit removeInputDevice(pDevice); + } else if (!hadInputs && hasInputs) { + m_inputDevices.append(pDevice); + emit addInputDevice(pDevice); + } + + if (hadOutputs && !hasOutputs) { + m_outputDevices.removeOne(pDevice); + emit removeOutputDevice(pDevice); + } else if (!hadOutputs && hasOutputs) { + m_outputDevices.append(pDevice); + emit addOutputDevice(pDevice); + } +} + /// Called when any of the combo boxes in this dialog are changed. Enables the /// apply button and marks that settings have been changed so that /// DlgPrefSound::slotApply knows to apply them. diff --git a/src/preferences/dialog/dlgprefsound.h b/src/preferences/dialog/dlgprefsound.h index 86a05ae32e15..58b1ce356153 100644 --- a/src/preferences/dialog/dlgprefsound.h +++ b/src/preferences/dialog/dlgprefsound.h @@ -3,13 +3,11 @@ #include #include "control/pollingcontrolproxy.h" -#include "defs_urls.h" #include "preferences/constants.h" #include "preferences/dialog/dlgpreferencepage.h" #include "preferences/dialog/ui_dlgprefsounddlg.h" #include "preferences/usersettings.h" #include "soundio/sounddevice.h" -#include "soundio/sounddevicestatus.h" #include "soundio/soundmanagerconfig.h" #include "util/parented_ptr.h" @@ -42,8 +40,14 @@ class DlgPrefSound : public DlgPreferencePage, public Ui::DlgPrefSoundDlg { void writePaths(SoundManagerConfig *config); void refreshOutputDevices(const QList& devices); void refreshInputDevices(const QList& devices); + void addOutputDevice(SoundDevicePointer pDevice); + void addInputDevice(SoundDevicePointer pDevice); + void removeOutputDevice(SoundDevicePointer pDevice); + void removeInputDevice(SoundDevicePointer pDevice); void updatingAPI(); void updatedAPI(); + void deviceRouteUpdated(const SoundDeviceId& device, const AudioPath* pPath); + void deviceChannelsUpdated(SoundDevicePointer devices); public slots: void slotUpdate() override; // called on show @@ -83,6 +87,9 @@ class DlgPrefSound : public DlgPreferencePage, public Ui::DlgPrefSoundDlg { void updateKeylockDualThreadingCheckbox(); void updateKeylockMultithreading(bool enabled); #endif + void addDevice(SoundDevicePointer pDevice); + void removeDevice(SoundDevicePointer pDevice); + void updateDeviceChannels(SoundDevicePointer pDevice); private: void initializePaths(); diff --git a/src/preferences/dialog/dlgprefsounditem.cpp b/src/preferences/dialog/dlgprefsounditem.cpp index a618f6a5e8da..ae96dcff1fae 100644 --- a/src/preferences/dialog/dlgprefsounditem.cpp +++ b/src/preferences/dialog/dlgprefsounditem.cpp @@ -5,6 +5,7 @@ #include "moc_dlgprefsounditem.cpp" #include "soundio/sounddevice.h" #include "soundio/soundmanagerconfig.h" +#include "util/assert.h" /// Constructs a new preferences sound item, representing an AudioPath and SoundDevice /// with a label and two combo boxes. @@ -68,6 +69,77 @@ void DlgPrefSoundItem::refreshDevices(const QList& devices) } } +void DlgPrefSoundItem::addDevice(const SoundDevicePointer pDevice) { + // SoundDeviceId oldDev = + // deviceComboBox->itemData(deviceComboBox->currentIndex()).value(); + deviceComboBox->addItem(pDevice->getDisplayName(), QVariant::fromValue(pDevice->getDeviceId())); + + // int newIndex = deviceComboBox->findData(QVariant::fromValue(oldDev)); + // deviceComboBox->setCurrentIndex(newIndex); + + m_devices.push_back(pDevice); +} + +void DlgPrefSoundItem::removeDevice(const SoundDevicePointer pDevice) { + int removeIndex = deviceComboBox->findData(QVariant::fromValue(pDevice->getDeviceId())); + int currentIndex = deviceComboBox->currentIndex(); + + if (currentIndex == removeIndex) { + deviceComboBox->setCurrentIndex(0); + deviceComboBox->removeItem(removeIndex); + } else { + SoundDeviceId oldDev = deviceComboBox->itemData(currentIndex).value(); + deviceComboBox->removeItem(removeIndex); + + int newIndex = deviceComboBox->findData(QVariant::fromValue(oldDev)); + if (newIndex != currentIndex) { + deviceComboBox->setCurrentIndex(newIndex); + } + } + m_devices.removeOne(pDevice); +} + +void DlgPrefSoundItem::updateDeviceChannels(SoundDevicePointer pDevice) { + const auto& id = pDevice->getDeviceId(); + int index = deviceComboBox->findData(QVariant::fromValue(id)); + if (index >= 0 && deviceComboBox->currentIndex() == index) { + // if changed device is not selected no need to update + int currentIndex = channelComboBox->currentIndex(); + auto channelData = channelComboBox->itemData(currentIndex).value(); + deviceChanged(index); + auto newIndex = channelComboBox->findData(QVariant::fromValue(channelData)); + + m_emitSettingChanged = false; + channelComboBox->setCurrentIndex(newIndex); + m_emitSettingChanged = true; + } +} + +void DlgPrefSoundItem::updateDeviceRoute(const SoundDeviceId& id, const AudioPath* pPath) { + if (pPath->getType() != m_type || pPath->getIndex() != m_index) { + return; + } + + // qWarning() << "DlgPrefSoundItem::updateDevice" << id.name; + int index = deviceComboBox->findData(QVariant::fromValue(id)); + + VERIFY_OR_DEBUG_ASSERT(index >= 0) { + return; + } + + if (index != deviceComboBox->currentIndex()) { + deviceComboBox->blockSignals(true); + deviceComboBox->setCurrentIndex(index); + deviceComboBox->blockSignals(false); + deviceChanged(index); + } + + auto channelGroup = pPath->getChannelGroup(); + QPoint point = QPoint(channelGroup.getChannelBase(), channelGroup.getChannelCount()); + int channelIndex = channelComboBox->findData(QVariant::fromValue(point)); + channelComboBox->setCurrentIndex(channelIndex); +} + /// Slot called when the device combo box selection changes. Updates the channel /// combo box. void DlgPrefSoundItem::deviceChanged(int index) { diff --git a/src/preferences/dialog/dlgprefsounditem.h b/src/preferences/dialog/dlgprefsounditem.h index 18501791c2b5..8c6d2959c871 100644 --- a/src/preferences/dialog/dlgprefsounditem.h +++ b/src/preferences/dialog/dlgprefsounditem.h @@ -32,6 +32,7 @@ class DlgPrefSoundItem : public QWidget, public Ui::DlgPrefSoundItem { return channelComboBox->currentIndex(); } void selectFirstUnusedChannelIndex(const QList& selectedChannels); + void setDevice(const SoundDeviceId& device); signals: void selectedDeviceChanged(); @@ -46,10 +47,13 @@ class DlgPrefSoundItem : public QWidget, public Ui::DlgPrefSoundItem { void writePath(SoundManagerConfig *config) const; void save(); void reload(); + void addDevice(SoundDevicePointer pDevice); + void removeDevice(SoundDevicePointer pDevice); + void updateDeviceChannels(SoundDevicePointer pDevice); + void updateDeviceRoute(const SoundDeviceId& pDevice, const AudioPath* pPath); private: SoundDevicePointer getDevice() const; // if this returns NULL, we don't have a valid AudioPath - void setDevice(const SoundDeviceId& device); void setChannel(unsigned int channelBase, unsigned int channels); int hasSufficientChannels(const SoundDevice& device) const; diff --git a/src/soundio/pipewireenumerator.cpp b/src/soundio/pipewireenumerator.cpp index 0163914d86c5..1380c161eca7 100644 --- a/src/soundio/pipewireenumerator.cpp +++ b/src/soundio/pipewireenumerator.cpp @@ -16,6 +16,7 @@ namespace { +constexpr int kCpuUsageUpdateRate = 30; // in 1/s, fits to display frame rate const QString kAppGroup = QStringLiteral("[App]"); static const char* find_node_name(const struct spa_dict* props) { @@ -41,6 +42,9 @@ PipewireEnumerator::PipewireEnumerator(UserSettingsPointer, SoundManager* pManag : m_pSoundManager(pManager), m_initialized(false), m_audioLatencyUsage(kAppGroup, QStringLiteral("audio_latency_usage")) { + connect(this, &PipewireEnumerator::deviceAdded, m_pSoundManager, &SoundManager::addDevice); + connect(this, &PipewireEnumerator::deviceRemoved, m_pSoundManager, &SoundManager::removeDevice); + pw_init(nullptr, nullptr); m_pThreadLoop = pw_thread_loop_new("mixxx_loop", nullptr); @@ -141,7 +145,7 @@ void PipewireEnumerator::registryEventGlobal(uint32_t id, m_objects.insert_or_assign(id, Object{Node{}}); auto pDevice = QSharedPointer::create( m_pConfig, m_pSoundManager, this, id, name); - m_pSoundManager->addDevice(pDevice); + emit deviceAdded(pDevice); m_soundDevices.insert_or_assign(id, std::move(pDevice)); if (strcmp(name, "Mixxx") == 0) { @@ -162,6 +166,7 @@ void PipewireEnumerator::registryEventGlobal(uint32_t id, m_objects.insert_or_assign(id, Object{Port(port_id, node_id, direction)}); auto& soundDevice = m_soundDevices[node_id]; soundDevice->registerDevicePort(id, pProps); + m_pSoundManager->updateDeviceChannels(soundDevice); if (node_id != m_filterId) { return; @@ -215,7 +220,7 @@ void PipewireEnumerator::registryEventGlobalRemove(unsigned int id) { if (device->isOpen()) { device->close(); } - m_pSoundManager->removeDevice(device); + emit deviceRemoved(device); } else if (auto* port = std::get_if(&object)) { auto& device = m_soundDevices[port->nodeId]; device->unregisterDevicePort(port->id, port->direction); @@ -382,6 +387,22 @@ void PipewireEnumerator::callback(const spa_io_position* pos) { soundDevice->writeOutput(static_cast(buffer), port.devicePort, framesPerBuffer); } } + updateAudioLatencyUsage(framesPerBuffer); +} + +void PipewireEnumerator::updateAudioLatencyUsage(const SINT framesPerBuffer) { + m_framesSinceAudioLatencyUsageUpdate += framesPerBuffer; + if (m_framesSinceAudioLatencyUsageUpdate > (m_sampleRate.toDouble() / kCpuUsageUpdateRate)) { + double secInAudioCb = m_timeInAudioCallback.toDoubleSeconds(); + m_audioLatencyUsage.set( + secInAudioCb / (m_framesSinceAudioLatencyUsageUpdate / m_sampleRate.toDouble())); + m_timeInAudioCallback = mixxx::Duration::fromSeconds(0); + m_framesSinceAudioLatencyUsageUpdate = 0; + // qDebug() << m_audioLatencyUsage + // << m_audioLatencyUsage->get(); + } + // measure time in Audio callback at the very last + m_timeInAudioCallback += m_clkRefTimer.elapsed(); } void PipewireEnumerator::createLink(uint32_t outNodeId, diff --git a/src/soundio/pipewireenumerator.h b/src/soundio/pipewireenumerator.h index 068fca33c685..cfa5c00f4430 100644 --- a/src/soundio/pipewireenumerator.h +++ b/src/soundio/pipewireenumerator.h @@ -5,6 +5,7 @@ #include #include +#include #include "preferences/usersettings.h" #include "soundio/sounddevice.h" @@ -34,6 +35,10 @@ class PipewireEnumerator : public SoundDeviceEnumerator { void openDevice(uint32_t id, std::set& inChannels, std::set& outChannels); void closeDevice(uint32_t id); + signals: + void deviceAdded(SoundDevicePointer pDevice); + void deviceRemoved(SoundDevicePointer pDevice); + private: static void registryEventGlobalOuter(void* data, uint32_t id, @@ -100,6 +105,8 @@ class PipewireEnumerator : public SoundDeviceEnumerator { uint32_t outPortId, uint32_t inNodeI, uint32_t inPortId); + void updateAudioLatencyUsage(const SINT framesPerBuffer); + std::unordered_map> m_soundDevices; struct Link { diff --git a/src/soundio/soundmanager.cpp b/src/soundio/soundmanager.cpp index 4fcf17848fd9..b85a55a2ade3 100644 --- a/src/soundio/soundmanager.cpp +++ b/src/soundio/soundmanager.cpp @@ -11,7 +11,6 @@ #include "soundio/networkenumerator.h" #include "soundio/portaudioenumerator.h" #include "soundio/sounddevice.h" -#include "soundio/sounddeviceenumerator.h" #include "soundio/sounddevicenetwork.h" #include "soundio/sounddevicenotfound.h" #include "soundio/sounddeviceportaudio.h" @@ -276,8 +275,6 @@ void SoundManager::queryDevices() { m_devices.push_back(device); qDebug() << "m_devices.push_back " << device->getDisplayName(); } - - m_pipewireEnumerator->initialize(); #endif for (auto& device : m_networkEnumerator->queryDevices()) { @@ -339,6 +336,7 @@ SoundDeviceStatus SoundManager::setupDevices() { QVector toOpen; bool haveOutput = false; // loop over all available devices + for (const auto& pDevice : std::as_const(m_devices)) { DeviceMode mode = {pDevice, false, false}; pDevice->clearInputs(); @@ -665,7 +663,6 @@ void SoundManager::addDevice(SoundDevicePointer pDevice) { emit deviceAdded(pDevice); } -// device closing is handled before this void SoundManager::removeDevice(SoundDevicePointer pDevice) { for (const auto& device : std::as_const(m_devices)) { if (device == pDevice) { @@ -677,7 +674,6 @@ void SoundManager::removeDevice(SoundDevicePointer pDevice) { } } -// device port/link changed -void SoundManager::updateDevice(SoundDevicePointer pDevice) { - emit deviceUpdated(pDevice); +void SoundManager::updateDeviceChannels(SoundDevicePointer pDevice) { + emit deviceChannelsUpdated(pDevice); } diff --git a/src/soundio/soundmanager.h b/src/soundio/soundmanager.h index a468cd285739..e61c4c696d3f 100644 --- a/src/soundio/soundmanager.h +++ b/src/soundio/soundmanager.h @@ -111,24 +111,29 @@ class SoundManager : public QObject { m_audioLatencyOverloadCount.set(0); } + // currently only used by pipewire + void updateDeviceChannels(SoundDevicePointer pDevice); + signals: + void deviceAdded(SoundDevicePointer pDevice); + void deviceRemoved(SoundDevicePointer pDevice); + void deviceChannelsUpdated(SoundDevicePointer pDevice); + void deviceConnected(const SoundDeviceId& pDevice, const AudioPath* pPath); + void deviceDisconnected(const AudioPath* pPath); + void devicesUpdated(); // emitted when pointers to SoundDevices go stale void devicesSetup(); // emitted when the sound devices have been set up void devicesClosed(); // emitted when the sound devices have been closed and resources freed void outputRegistered(const AudioOutput& output, AudioSource* src); void inputRegistered(const AudioInput& input, AudioDestination* dest); - void deviceAdded(SoundDevicePointer device); - void deviceRemoved(SoundDevicePointer device); - void deviceUpdated(SoundDevicePointer device); - - public slots: - void addDevice(SoundDevicePointer device); - void removeDevice(SoundDevicePointer device); - void updateDevice(SoundDevicePointer device); private slots: void completeDevicesClosing(); + public slots: + void addDevice(SoundDevicePointer pDevice); + void removeDevice(SoundDevicePointer pDevice); + private: // Closes all the devices and empties the list of devices we have. void clearDeviceList(bool sleepAfterClosing); From 185d7a53398cdb961a9ac668ccbac2aea367d92a Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Wed, 17 Jun 2026 19:42:49 +0530 Subject: [PATCH 04/13] infer pw_filter port index from port names instead of unreliable PW_KEY_PORT_ID --- src/soundio/pipewireenumerator.cpp | 35 +++++++++++++++++++++++++---- src/soundio/pipewireenumerator.h | 6 ----- src/soundio/sounddevicepipewire.cpp | 22 +++++++++--------- src/soundio/sounddevicepipewire.h | 2 +- 4 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/soundio/pipewireenumerator.cpp b/src/soundio/pipewireenumerator.cpp index 1380c161eca7..9d5ad58a0314 100644 --- a/src/soundio/pipewireenumerator.cpp +++ b/src/soundio/pipewireenumerator.cpp @@ -36,6 +36,30 @@ static const char* find_node_name(const struct spa_dict* props) { return nullptr; } +// Calculates the port index from port name, since port name is very +// convenient property to access. That also mean that any changes in +// port naming scheme take in account this function. +static std::optional getPortIndexFromName(const char* name) { + std::string_view view(name); + + auto pos = view.find(':'); + if (pos == std::string_view::npos) { + return std::nullopt; + } + + uint32_t value; + auto [ptr, ec] = std::from_chars( + view.data() + pos + 1, + view.data() + view.size(), + value); + + if (ec == std::errc{}) { + return value; + } + + return std::nullopt; +} + } // namespace PipewireEnumerator::PipewireEnumerator(UserSettingsPointer, SoundManager* pManager) @@ -153,18 +177,16 @@ void PipewireEnumerator::registryEventGlobal(uint32_t id, } } else if (strcmp(pType, PW_TYPE_INTERFACE_Port) == 0) { const uint32_t node_id = pw_properties_parse_int(spa_dict_lookup(pProps, PW_KEY_NODE_ID)); - const uint32_t port_id = pw_properties_parse_int(spa_dict_lookup(pProps, PW_KEY_PORT_ID)); const char* dir = spa_dict_lookup(pProps, PW_KEY_PORT_DIRECTION); const bool isInput = strcmp(dir, "in") == 0; - const spa_direction direction = isInput ? SPA_DIRECTION_INPUT : SPA_DIRECTION_OUTPUT; if (!m_soundDevices.contains(node_id)) { // most likely midi or video node return; } - m_objects.insert_or_assign(id, Object{Port(port_id, node_id, direction)}); - auto& soundDevice = m_soundDevices[node_id]; + m_objects.insert_or_assign(id, Object{Port(node_id)}); + auto soundDevice = m_soundDevices[node_id]; soundDevice->registerDevicePort(id, pProps); m_pSoundManager->updateDeviceChannels(soundDevice); @@ -172,6 +194,11 @@ void PipewireEnumerator::registryEventGlobal(uint32_t id, return; } + auto port_id = getPortIndexFromName(spa_dict_lookup(pProps, PW_KEY_PORT_NAME)); + VERIFY_OR_DEBUG_ASSERT(!port_id.has_value()) { + return; + } + for (auto& [deviceId, device] : m_openedDevices) { if (isInput) { for (auto& port : device.inputs) { diff --git a/src/soundio/pipewireenumerator.h b/src/soundio/pipewireenumerator.h index cfa5c00f4430..25f2aff76861 100644 --- a/src/soundio/pipewireenumerator.h +++ b/src/soundio/pipewireenumerator.h @@ -115,13 +115,7 @@ class PipewireEnumerator : public SoundDeviceEnumerator { }; struct Port { - // this is not global id, but port.id, which starts from 0 - // using this we can directly index into the port vector - // on the node from nodeId - // global id indexes into maps, port.id indexes into vectors - uint32_t id; uint32_t nodeId; - spa_direction direction; }; struct Node {}; diff --git a/src/soundio/sounddevicepipewire.cpp b/src/soundio/sounddevicepipewire.cpp index 9498faabb986..8f83a5f783a9 100644 --- a/src/soundio/sounddevicepipewire.cpp +++ b/src/soundio/sounddevicepipewire.cpp @@ -140,15 +140,17 @@ void SoundDevicePipewire::registerDevicePort(uint32_t id, const struct spa_dict* } } -void SoundDevicePipewire::unregisterDevicePort(uint32_t id, spa_direction direction) { - switch (direction) { - case SPA_DIRECTION_INPUT: - m_inPorts.erase(m_inPorts.begin() + id); - m_numInputChannels = mixxx::audio::ChannelCount::fromInt(m_outPorts.size()); - break; - case SPA_DIRECTION_OUTPUT: - m_outPorts.erase(m_outPorts.begin() + id); - m_numOutputChannels = mixxx::audio::ChannelCount::fromInt(m_inPorts.size()); - break; +void SoundDevicePipewire::unregisterDevicePort(uint32_t id) { + for (auto it = m_inPorts.begin(); it != m_inPorts.end(); it++) { + if (it->id == id) { + m_inPorts.erase(it); + return; + } + } + for (auto it = m_outPorts.begin(); it != m_outPorts.end(); it++) { + if (it->id == id) { + m_outPorts.erase(it); + return; + } } } diff --git a/src/soundio/sounddevicepipewire.h b/src/soundio/sounddevicepipewire.h index 721be27a344a..02233f0c86e5 100644 --- a/src/soundio/sounddevicepipewire.h +++ b/src/soundio/sounddevicepipewire.h @@ -39,7 +39,7 @@ class SoundDevicePipewire : public SoundDevice { uint32_t inNodeId, uint32_t inPortId); void registerDevicePort(uint32_t id, const struct spa_dict* props); - void unregisterDevicePort(uint32_t id, spa_direction direction); + void unregisterDevicePort(uint32_t id); struct Port { uint32_t id; From cb3b805b14b2fe36d3d54c3c2f7d4fb1e4b4d59e Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Fri, 19 Jun 2026 16:47:32 +0530 Subject: [PATCH 05/13] Use std::atomic to load and store state accessed in callback --- src/soundio/pipewireenumerator.cpp | 136 +++++++++++++++++++---------- src/soundio/pipewireenumerator.h | 8 +- 2 files changed, 93 insertions(+), 51 deletions(-) diff --git a/src/soundio/pipewireenumerator.cpp b/src/soundio/pipewireenumerator.cpp index 9d5ad58a0314..ff36e58b4cb9 100644 --- a/src/soundio/pipewireenumerator.cpp +++ b/src/soundio/pipewireenumerator.cpp @@ -1,10 +1,11 @@ #include "soundio/pipewireenumerator.h" #include -#include #include #include +#include + #include "moc_pipewireenumerator.cpp" #include "soundio/sounddevice.h" #include "soundio/sounddevicepipewire.h" @@ -64,6 +65,8 @@ static std::optional getPortIndexFromName(const char* name) { PipewireEnumerator::PipewireEnumerator(UserSettingsPointer, SoundManager* pManager) : m_pSoundManager(pManager), + m_soundDevices(std::make_shared()), + m_openedDevices(std::make_shared()), m_initialized(false), m_audioLatencyUsage(kAppGroup, QStringLiteral("audio_latency_usage")) { connect(this, &PipewireEnumerator::deviceAdded, m_pSoundManager, &SoundManager::addDevice); @@ -170,7 +173,12 @@ void PipewireEnumerator::registryEventGlobal(uint32_t id, auto pDevice = QSharedPointer::create( m_pConfig, m_pSoundManager, this, id, name); emit deviceAdded(pDevice); - m_soundDevices.insert_or_assign(id, std::move(pDevice)); + auto pSoundDevices = std::make_shared(*m_soundDevices.load()); + + // pipewire assigns each object with a unique ID + // any previous element is either invalid or already removed + pSoundDevices->insert_or_assign(id, std::move(pDevice)); + m_soundDevices.store(pSoundDevices); if (strcmp(name, "Mixxx") == 0) { m_filterId = id; @@ -180,38 +188,41 @@ void PipewireEnumerator::registryEventGlobal(uint32_t id, const char* dir = spa_dict_lookup(pProps, PW_KEY_PORT_DIRECTION); const bool isInput = strcmp(dir, "in") == 0; - if (!m_soundDevices.contains(node_id)) { + if (!m_soundDevices.load()->contains(node_id)) { // most likely midi or video node return; } m_objects.insert_or_assign(id, Object{Port(node_id)}); - auto soundDevice = m_soundDevices[node_id]; - soundDevice->registerDevicePort(id, pProps); - m_pSoundManager->updateDeviceChannels(soundDevice); + auto pSoundDevices = m_soundDevices.load(); + auto pSoundDevice = pSoundDevices->at(node_id); + pSoundDevice->registerDevicePort(id, pProps); + m_pSoundManager->updateDeviceChannels(pSoundDevice); if (node_id != m_filterId) { return; } - auto port_id = getPortIndexFromName(spa_dict_lookup(pProps, PW_KEY_PORT_NAME)); - VERIFY_OR_DEBUG_ASSERT(!port_id.has_value()) { + auto portId = getPortIndexFromName(spa_dict_lookup(pProps, PW_KEY_PORT_NAME)); + VERIFY_OR_DEBUG_ASSERT(portId.has_value()) { return; } - for (auto& [deviceId, device] : m_openedDevices) { + auto pOpenedDevices = *m_openedDevices.load(); + + for (auto& [deviceId, device] : pOpenedDevices) { if (isInput) { for (auto& port : device.inputs) { - if (port.filterPort == port_id) { - auto devicePorts = m_soundDevices[deviceId]->getOutPorts(); + if (port.filterPort == portId) { + auto devicePorts = pSoundDevices->at(deviceId)->getOutPorts(); uint32_t devicePortId = devicePorts[port.devicePort].id; createLink(deviceId, devicePortId, node_id, id); } } } else { for (auto& port : device.outputs) { - if (port_id == port.filterPort) { - auto devicePorts = m_soundDevices[deviceId]->getInPorts(); + if (portId == port.filterPort) { + auto devicePorts = pSoundDevices->at(deviceId)->getInPorts(); uint32_t devicePortId = devicePorts[port.devicePort].id; createLink(node_id, id, deviceId, devicePortId); } @@ -239,27 +250,44 @@ void PipewireEnumerator::registryEventGlobalRemove(unsigned int id) { return; } - Object& object = m_objects.at(id); + auto pair = m_objects.extract(id); + Object& object = pair.mapped(); if (std::get_if(&object)) { - const auto& node = m_soundDevices.extract(id); - auto& device = node.mapped(); - if (device->isOpen()) { - device->close(); + auto pSoundDevices = std::make_shared(*m_soundDevices.load()); + if (!pSoundDevices->contains(id)) { + return; } - emit deviceRemoved(device); + + auto pDevice = pSoundDevices->at(id); + if (pDevice->isOpen()) { + pDevice->close(); + } + + qWarning() << "removing device:" << pDevice->getDisplayName(); + pSoundDevices->erase(id); + m_soundDevices.store(pSoundDevices); + emit deviceRemoved(pDevice); + // m_pSoundManager->removeDevice(device); } else if (auto* port = std::get_if(&object)) { - auto& device = m_soundDevices[port->nodeId]; - device->unregisterDevicePort(port->id, port->direction); - } + auto pSoundDevices = m_soundDevices.load(); + VERIFY_OR_DEBUG_ASSERT(pSoundDevices->contains(port->nodeId)) { + qWarning() << "node" << port->nodeId << "port " << id; + return; + } - m_objects.erase(id); + auto pSoundDevice = pSoundDevices->at(port->nodeId); + qWarning() << "removing port:" << id; + pSoundDevice->unregisterDevicePort(id); + m_pSoundManager->updateDeviceChannels(pSoundDevice); + } } std::vector PipewireEnumerator::queryDevices() const { - std::vector devices{}; - for (const auto& [id, device] : m_soundDevices) { - devices.push_back(device); + std::vector devices; + auto pSoundDevices = m_soundDevices.load(); + for (const auto& [id, pDevice] : *pSoundDevices) { + devices.push_back(pDevice); } return devices; @@ -289,12 +317,14 @@ int PipewireEnumerator::metadataProperty( } bool PipewireEnumerator::isOpen(uint32_t id) { - return m_openedDevices.contains(id); + return m_openedDevices.load()->contains(id); } void PipewireEnumerator::openDevice( uint32_t id, const std::set& inChans, const std::set& outChans) { - VERIFY_OR_DEBUG_ASSERT(!m_openedDevices.contains(id)) { + auto pOpenedDevices = std::make_shared(*m_openedDevices.load()); + + VERIFY_OR_DEBUG_ASSERT(!pOpenedDevices->contains(id)) { qWarning() << "device:" << id << "already open"; return; } @@ -304,7 +334,7 @@ void PipewireEnumerator::openDevice( size_t numInPorts = 0; size_t numOutPorts = 0; - for (auto& [id, device] : m_openedDevices) { + for (auto& [id, device] : *pOpenedDevices) { numInPorts += device.inputs.size(); numOutPorts += device.outputs.size(); } @@ -348,16 +378,18 @@ void PipewireEnumerator::openDevice( } pw_thread_loop_unlock(m_pThreadLoop); - m_openedDevices.emplace(id, Device{std::move(inputs), std::move(outputs)}); + pOpenedDevices->emplace(id, Device{std::move(inputs), std::move(outputs)}); + m_openedDevices.store(pOpenedDevices); } void PipewireEnumerator::closeDevice(uint32_t id) { - VERIFY_OR_DEBUG_ASSERT(m_openedDevices.contains(id)) { + auto pOpenedDevices = std::make_shared(*m_openedDevices.load()); + VERIFY_OR_DEBUG_ASSERT(pOpenedDevices->contains(id)) { qWarning() << "device:" << id << "not opened"; return; } - auto& device = m_openedDevices[id]; + auto& device = pOpenedDevices->at(id); pw_thread_loop_lock(m_pThreadLoop); for (auto& port : device.inputs) { @@ -369,7 +401,8 @@ void PipewireEnumerator::closeDevice(uint32_t id) { } pw_thread_loop_unlock(m_pThreadLoop); - m_openedDevices.erase(id); + pOpenedDevices->erase(id); + m_openedDevices.store(pOpenedDevices); } void PipewireEnumerator::callback(const spa_io_position* pos) { @@ -390,28 +423,35 @@ void PipewireEnumerator::callback(const spa_io_position* pos) { const uint64_t framesPerBuffer = pos->clock.duration; m_pSoundManager->processUnderflowHappened(framesPerBuffer); - for (auto& [id, device] : m_openedDevices) { - auto soundDevice = m_soundDevices[id]; + auto pOpenedDevices = m_openedDevices.load(); + auto pSoundDevices = m_soundDevices.load(); + + for (auto& [id, device] : *pOpenedDevices) { + auto pSoundDevice = pSoundDevices->at(id); auto& ports = device.inputs; for (const auto& port : ports) { - void* buffer = pw_filter_get_dsp_buffer(port.pPortData, framesPerBuffer); - soundDevice->writeInput(static_cast(buffer), port.devicePort, framesPerBuffer); + void* pBuffer = pw_filter_get_dsp_buffer(port.pPortData, framesPerBuffer); + pSoundDevice->writeInput(static_cast(pBuffer), + port.devicePort, + framesPerBuffer); } - m_pSoundManager->pushInputBuffers(soundDevice->inputs(), framesPerBuffer); + m_pSoundManager->pushInputBuffers(pSoundDevice->inputs(), framesPerBuffer); } m_pSoundManager->onDeviceOutputCallback(framesPerBuffer); - for (auto& [id, device] : m_openedDevices) { - auto& soundDevice = m_soundDevices[id]; + for (auto& [id, device] : *pOpenedDevices) { + auto pSoundDevice = pSoundDevices->at(id); auto& ports = device.outputs; for (const auto& port : ports) { - void* buffer = pw_filter_get_dsp_buffer(port.pPortData, framesPerBuffer); - if (!buffer) { + void* pBuffer = pw_filter_get_dsp_buffer(port.pPortData, framesPerBuffer); + if (!pBuffer) { continue; } - SampleUtil::clear(static_cast(buffer), framesPerBuffer); - soundDevice->writeOutput(static_cast(buffer), port.devicePort, framesPerBuffer); + SampleUtil::clear(static_cast(pBuffer), framesPerBuffer); + pSoundDevice->writeOutput(static_cast(pBuffer), + port.devicePort, + framesPerBuffer); } } updateAudioLatencyUsage(framesPerBuffer); @@ -450,13 +490,13 @@ void PipewireEnumerator::createLink(uint32_t outNodeId, items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_INPUT_PORT, strInPort.c_str()); items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_LINGER, "true"); - struct pw_proxy* proxy = (struct pw_proxy*)pw_core_create_object(m_pCore, + struct pw_proxy* pProxy = static_cast(pw_core_create_object(m_pCore, "link-factory", PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, &props, - 0); - if (proxy) { - pw_proxy_destroy(proxy); + 0)); + if (pProxy) { + pw_proxy_destroy(pProxy); } } diff --git a/src/soundio/pipewireenumerator.h b/src/soundio/pipewireenumerator.h index 25f2aff76861..7cfe2d0981f0 100644 --- a/src/soundio/pipewireenumerator.h +++ b/src/soundio/pipewireenumerator.h @@ -107,8 +107,6 @@ class PipewireEnumerator : public SoundDeviceEnumerator { uint32_t inPortId); void updateAudioLatencyUsage(const SINT framesPerBuffer); - std::unordered_map> m_soundDevices; - struct Link { uint32_t input; uint32_t output; @@ -153,7 +151,11 @@ class PipewireEnumerator : public SoundDeviceEnumerator { std::vector outputs; }; - std::unordered_map m_openedDevices; + using SoundDeviceMap = std::unordered_map>; + std::atomic> m_soundDevices; + + using DeviceMap = std::unordered_map; + std::atomic> m_openedDevices; bool m_initialized; uint64_t xrun_duration; From 481a5556281b307d61e1e7f4e1a7a1d2b8de0275 Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Sat, 20 Jun 2026 11:57:40 +0530 Subject: [PATCH 06/13] Respect pipewire clock.rate and clock.allowed-rates metadata Use Mixxx samplerate and buffer size for PipeWire filter --- src/preferences/dialog/dlgprefsound.cpp | 22 ++++++++----- src/preferences/dialog/dlgprefsound.h | 1 + src/soundio/pipewireenumerator.cpp | 44 ++++++++++++++++++------- src/soundio/pipewireenumerator.h | 10 +++++- src/soundio/portaudioenumerator.cpp | 5 +-- src/soundio/sounddevicepipewire.cpp | 17 +++++++++- src/soundio/sounddevicepipewire.h | 4 +-- src/soundio/soundmanager.cpp | 14 +++++--- 8 files changed, 83 insertions(+), 34 deletions(-) diff --git a/src/preferences/dialog/dlgprefsound.cpp b/src/preferences/dialog/dlgprefsound.cpp index 3ce9ecabcaaf..c7130c771a9c 100644 --- a/src/preferences/dialog/dlgprefsound.cpp +++ b/src/preferences/dialog/dlgprefsound.cpp @@ -126,16 +126,8 @@ DlgPrefSound::DlgPrefSound(QWidget* pParent, QStringLiteral("(?)"), MIXXX_MANUAL_SOUND_API_URL)); - sampleRateComboBox->clear(); const auto sampleRates = m_pSoundManager->getSampleRates(); - for (const auto& sampleRate : sampleRates) { - if (sampleRate.isValid()) { - // no ridiculous sample rate values. prohibiting zero means - // avoiding a potential div-by-0 error in ::updateLatencies - sampleRateComboBox->addItem(tr("%1 Hz").arg(sampleRate.value()), - QVariant::fromValue(sampleRate)); - } - } + updateSampleRates(sampleRates); connect(sampleRateComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, @@ -1182,3 +1174,15 @@ void DlgPrefSound::checkLatencyCompensation() { bool DlgPrefSound::okayToClose() const { return m_configValid; } + +void DlgPrefSound::updateSampleRates(const QList& sampleRates) { + sampleRateComboBox->clear(); + for (const auto& sampleRate : sampleRates) { + if (sampleRate.isValid()) { + // no ridiculous sample rate values. prohibiting zero means + // avoiding a potential div-by-0 error in ::updateLatencies + sampleRateComboBox->addItem(tr("%1 Hz").arg(sampleRate.value()), + QVariant::fromValue(sampleRate)); + } + } +} diff --git a/src/preferences/dialog/dlgprefsound.h b/src/preferences/dialog/dlgprefsound.h index 58b1ce356153..490e9a9fd0ee 100644 --- a/src/preferences/dialog/dlgprefsound.h +++ b/src/preferences/dialog/dlgprefsound.h @@ -90,6 +90,7 @@ class DlgPrefSound : public DlgPreferencePage, public Ui::DlgPrefSoundDlg { void addDevice(SoundDevicePointer pDevice); void removeDevice(SoundDevicePointer pDevice); void updateDeviceChannels(SoundDevicePointer pDevice); + void updateSampleRates(const QList& sampleRates); private: void initializePaths(); diff --git a/src/soundio/pipewireenumerator.cpp b/src/soundio/pipewireenumerator.cpp index ff36e58b4cb9..14e70a7924fd 100644 --- a/src/soundio/pipewireenumerator.cpp +++ b/src/soundio/pipewireenumerator.cpp @@ -5,7 +5,9 @@ #include #include +#include +#include "audio/types.h" #include "moc_pipewireenumerator.cpp" #include "soundio/sounddevice.h" #include "soundio/sounddevicepipewire.h" @@ -127,14 +129,6 @@ PipewireEnumerator::~PipewireEnumerator() { } QList PipewireEnumerator::getSampleRates() const { - if (m_samplerates.empty()) { - return QList{ - mixxx::audio::SampleRate(44100), - mixxx::audio::SampleRate(48000), - mixxx::audio::SampleRate(96000), - }; - } - return m_samplerates; } @@ -300,7 +294,10 @@ int PipewireEnumerator::metadataProperty( void* data, uint32_t, const char* key, const char*, const char* value) { PipewireEnumerator* pEnumerator = static_cast(data); - if (strcmp(key, "clock.allowed-rates") == 0) { + if (strcmp(key, "clock.rate") == 0) { + pEnumerator->m_defaultSampleRate = mixxx::audio::SampleRate(std::atoi(value)); + } else if (strcmp(key, "clock.allowed-rates") == 0) { + qDebug() << "PipewireEnumerator::metadataProperty clock.allowed-rates" << value; // parse json arrays like [ 44100, 48000, 96000 ] QString s = value; s.remove('['); @@ -311,7 +308,6 @@ int PipewireEnumerator::metadataProperty( for (const QString& part : parts) { pEnumerator->m_samplerates.push_back(mixxx::audio::SampleRate(part.trimmed().toInt())); } - pEnumerator->m_pSoundManager->checkConfig(); } return 0; } @@ -320,8 +316,11 @@ bool PipewireEnumerator::isOpen(uint32_t id) { return m_openedDevices.load()->contains(id); } -void PipewireEnumerator::openDevice( - uint32_t id, const std::set& inChans, const std::set& outChans) { +void PipewireEnumerator::openDevice(uint32_t id, + const std::set& inChans, + const std::set& outChans, + mixxx::audio::SampleRate rate, + uint32_t framesPerBuffer) { auto pOpenedDevices = std::make_shared(*m_openedDevices.load()); VERIFY_OR_DEBUG_ASSERT(!pOpenedDevices->contains(id)) { @@ -331,6 +330,23 @@ void PipewireEnumerator::openDevice( pw_thread_loop_lock(m_pThreadLoop); + if (rate != m_sampleRate.value() || framesPerBuffer != m_framesPerBuffer) { + std::string rateStr = "1/" + std::to_string(rate); + std::string latencyStr = std::to_string(framesPerBuffer) + "/" + std::to_string(rate); + + spa_dict_item items[] = { + SPA_DICT_ITEM_INIT(PW_KEY_NODE_RATE, rateStr.c_str()), + SPA_DICT_ITEM_INIT(PW_KEY_NODE_LATENCY, latencyStr.c_str()), + }; + spa_dict properties = SPA_DICT_INIT(items, 2); + + int res = pw_filter_update_properties(m_pFilter, nullptr, &properties); + if (res >= 0) { + m_sampleRate = mixxx::audio::SampleRate(rate); + m_framesPerBuffer = framesPerBuffer; + } + } + size_t numInPorts = 0; size_t numOutPorts = 0; @@ -500,3 +516,7 @@ void PipewireEnumerator::createLink(uint32_t outNodeId, pw_proxy_destroy(pProxy); } } + +mixxx::audio::SampleRate PipewireEnumerator::getDefaultSampleRate() const { + return m_defaultSampleRate; +} diff --git a/src/soundio/pipewireenumerator.h b/src/soundio/pipewireenumerator.h index 7cfe2d0981f0..63ce9f684a52 100644 --- a/src/soundio/pipewireenumerator.h +++ b/src/soundio/pipewireenumerator.h @@ -32,8 +32,13 @@ class PipewireEnumerator : public SoundDeviceEnumerator { pw_thread_loop* getThreadLoop(); bool isOpen(uint32_t id); - void openDevice(uint32_t id, std::set& inChannels, std::set& outChannels); + void openDevice(uint32_t id, + const std::set& inChans, + const std::set& outChans, + mixxx::audio::SampleRate rate, + uint32_t framesPerBuffer); void closeDevice(uint32_t id); + mixxx::audio::SampleRate getDefaultSampleRate() const; signals: void deviceAdded(SoundDevicePointer pDevice); @@ -163,6 +168,9 @@ class PipewireEnumerator : public SoundDeviceEnumerator { double m_lastCallbackEntrytoDacSecs; PerformanceTimer m_clkRefTimer; mixxx::audio::SampleRate m_sampleRate; + mixxx::audio::SampleRate m_defaultSampleRate; + + uint32_t m_framesPerBuffer; PollingControlProxy m_audioLatencyUsage; mixxx::Duration m_timeInAudioCallback; diff --git a/src/soundio/portaudioenumerator.cpp b/src/soundio/portaudioenumerator.cpp index 58c6132f9f07..3112ec64a5e5 100644 --- a/src/soundio/portaudioenumerator.cpp +++ b/src/soundio/portaudioenumerator.cpp @@ -322,10 +322,7 @@ std::vector PortAudioEnumerator::getAPIs() const { QList PortAudioEnumerator::getSampleRates() const { // Hack because PortAudio samplerate enumeration is slow as hell on Linux // (ALSA dmix sucks, so we can't blame PortAudio) - return QList{ - mixxx::audio::SampleRate(44100), - mixxx::audio::SampleRate(48000), - mixxx::audio::SampleRate(96000)}; + return QList{}; } QList PortAudioEnumerator::getJackSampleRates() const { diff --git a/src/soundio/sounddevicepipewire.cpp b/src/soundio/sounddevicepipewire.cpp index 8f83a5f783a9..a7d0ab2de24d 100644 --- a/src/soundio/sounddevicepipewire.cpp +++ b/src/soundio/sounddevicepipewire.cpp @@ -4,6 +4,7 @@ #include "soundio/pipewireenumerator.h" #include "soundio/sounddevice.h" +#include "soundio/soundmanagerconfig.h" #include "util/sample.h" SoundDevicePipewire::SoundDevicePipewire(UserSettingsPointer pConfig, @@ -19,6 +20,7 @@ SoundDevicePipewire::SoundDevicePipewire(UserSettingsPointer pConfig, m_strDisplayName = QString::fromUtf8(name); m_numInputChannels = mixxx::audio::ChannelCount(0); m_numOutputChannels = mixxx::audio::ChannelCount(0); + m_sampleRate = getDefaultSampleRate(); } SoundDeviceStatus SoundDevicePipewire::open(bool, int) { @@ -40,7 +42,11 @@ SoundDeviceStatus SoundDevicePipewire::open(bool, int) { } } - m_pEnumerator->openDevice(m_deviceId.deviceIndex, inChans, outChans); + m_pEnumerator->openDevice(m_deviceId.deviceIndex, + inChans, + outChans, + m_sampleRate, + m_configFramesPerBuffer); return SoundDeviceStatus::Ok; } @@ -154,3 +160,12 @@ void SoundDevicePipewire::unregisterDevicePort(uint32_t id) { } } } + +mixxx::audio::SampleRate SoundDevicePipewire::getDefaultSampleRate() const { + auto defaultSampleRate = m_pEnumerator->getDefaultSampleRate(); + if (defaultSampleRate.isValid()) { + return defaultSampleRate; + } + + return SoundManagerConfig::kMixxxDefaultSampleRate; +} diff --git a/src/soundio/sounddevicepipewire.h b/src/soundio/sounddevicepipewire.h index 02233f0c86e5..1e73f7d37639 100644 --- a/src/soundio/sounddevicepipewire.h +++ b/src/soundio/sounddevicepipewire.h @@ -27,9 +27,7 @@ class SoundDevicePipewire : public SoundDevice { return {}; } - mixxx::audio::SampleRate getDefaultSampleRate() const override { - return SoundManagerConfig::kMixxxDefaultSampleRate; - } + mixxx::audio::SampleRate getDefaultSampleRate() const override; void writeOutput(float* output, int channel, int framesPerBuffer, int offset = 0); void writeInput(const float* input, int channel, int framesPerBuffer, int offset = 0); diff --git a/src/soundio/soundmanager.cpp b/src/soundio/soundmanager.cpp index b85a55a2ade3..940fc251aeda 100644 --- a/src/soundio/soundmanager.cpp +++ b/src/soundio/soundmanager.cpp @@ -235,19 +235,25 @@ void SoundManager::clearDeviceList(bool sleepAfterClosing) { } QList SoundManager::getSampleRates(const QString& api) const { + QList samplerates; if (api == MIXXX_PORTAUDIO_JACK_STRING) { // queryDevices must have been called for this to work, but the // ctor calls it -bkgood - return m_paEnumerator->getJackSampleRates(); + samplerates = m_paEnumerator->getJackSampleRates(); } #ifdef __PIPEWIRE__ else if (api == MIXXX_PIPEWIRE_STRING) { - return m_pipewireEnumerator->getSampleRates(); + samplerates = m_pipewireEnumerator->getSampleRates(); } #endif else if (!api.isEmpty()) { - return m_paEnumerator->getSampleRates(); + samplerates = m_paEnumerator->getSampleRates(); } + + if (!samplerates.empty()) { + return samplerates; + } + return QList{ mixxx::audio::SampleRate(44100), mixxx::audio::SampleRate(48000), @@ -256,7 +262,7 @@ QList SoundManager::getSampleRates(const QString& api) } QList SoundManager::getSampleRates() const { - return getSampleRates(""); + return getSampleRates(m_config.getAPI()); } void SoundManager::queryDevices() { From bea4b274be241ccb9870228d3953c6eb7c726007 Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Sat, 20 Jun 2026 13:45:10 +0530 Subject: [PATCH 07/13] Handle cases when PipeWire server is not running --- src/soundio/pipewireenumerator.cpp | 57 ++++++++++++++++++++++-------- src/soundio/pipewireenumerator.h | 5 +-- src/soundio/soundmanager.cpp | 30 +++++++++------- src/soundio/soundmanager.h | 8 ++--- 4 files changed, 64 insertions(+), 36 deletions(-) diff --git a/src/soundio/pipewireenumerator.cpp b/src/soundio/pipewireenumerator.cpp index 14e70a7924fd..9a959136dfc9 100644 --- a/src/soundio/pipewireenumerator.cpp +++ b/src/soundio/pipewireenumerator.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -78,7 +79,27 @@ PipewireEnumerator::PipewireEnumerator(UserSettingsPointer, SoundManager* pManag m_pThreadLoop = pw_thread_loop_new("mixxx_loop", nullptr); m_pContext = pw_context_new(pw_thread_loop_get_loop(m_pThreadLoop), nullptr, 0); +} + +PipewireEnumerator::~PipewireEnumerator() { + pw_thread_loop_stop(m_pThreadLoop); + spa_hook_remove(&m_registryListener); + spa_hook_remove(&m_metadataListener); + pw_proxy_destroy((struct pw_proxy*)m_pRegistry); + pw_proxy_destroy((struct pw_proxy*)m_pMetadata); + pw_core_disconnect(m_pCore); + pw_context_destroy(m_pContext); + pw_thread_loop_destroy(m_pThreadLoop); + pw_deinit(); +} + +void PipewireEnumerator::initialize() { m_pCore = pw_context_connect(m_pContext, nullptr, 0); + + if (!m_pCore) { + return; + } + m_pRegistry = pw_core_get_registry(m_pCore, PW_VERSION_REGISTRY, 0); // see https://docs.pipewire.org/page_man_pipewire-props_7.html @@ -108,24 +129,18 @@ PipewireEnumerator::PipewireEnumerator(UserSettingsPointer, SoundManager* pManag pw_registry_add_listener(m_pRegistry, &m_registryListener, ®istry_events, this); pw_filter_add_listener(m_pFilter, &m_filterListener, &filter_events, this); - pw_filter_connect(m_pFilter, + int res = pw_filter_connect(m_pFilter, PW_FILTER_FLAG_RT_PROCESS, nullptr, 0); + VERIFY_OR_DEBUG_ASSERT(res >= 0) { + qWarning() << "pw_filter_connect error:" << spa_strerror(res); + } + pw_thread_loop_start(m_pThreadLoop); -} -PipewireEnumerator::~PipewireEnumerator() { - pw_thread_loop_stop(m_pThreadLoop); - spa_hook_remove(&m_registryListener); - spa_hook_remove(&m_metadataListener); - pw_proxy_destroy((struct pw_proxy*)m_pRegistry); - pw_proxy_destroy((struct pw_proxy*)m_pMetadata); - pw_core_disconnect(m_pCore); - pw_context_destroy(m_pContext); - pw_thread_loop_destroy(m_pThreadLoop); - pw_deinit(); + m_initialized = true; } QList PipewireEnumerator::getSampleRates() const { @@ -287,9 +302,6 @@ std::vector PipewireEnumerator::queryDevices() const { return devices; } -void PipewireEnumerator::initialize() { -} - int PipewireEnumerator::metadataProperty( void* data, uint32_t, const char* key, const char*, const char* value) { PipewireEnumerator* pEnumerator = static_cast(data); @@ -321,6 +333,12 @@ void PipewireEnumerator::openDevice(uint32_t id, const std::set& outChans, mixxx::audio::SampleRate rate, uint32_t framesPerBuffer) { + VERIFY_OR_DEBUG_ASSERT(m_initialized) { + qWarning() << "PipewireEnumerator::openDevice called when " + "uninitialized, this should not happen"; + return; + } + auto pOpenedDevices = std::make_shared(*m_openedDevices.load()); VERIFY_OR_DEBUG_ASSERT(!pOpenedDevices->contains(id)) { @@ -344,6 +362,9 @@ void PipewireEnumerator::openDevice(uint32_t id, if (res >= 0) { m_sampleRate = mixxx::audio::SampleRate(rate); m_framesPerBuffer = framesPerBuffer; + } else { + qWarning() << "pw_filter_update_properties failed:" << spa_strerror(res); + qWarning() << "Unable to set requested samplerate and buffer size"; } } @@ -399,6 +420,12 @@ void PipewireEnumerator::openDevice(uint32_t id, } void PipewireEnumerator::closeDevice(uint32_t id) { + VERIFY_OR_DEBUG_ASSERT(m_initialized) { + qWarning() << "PipewireEnumerator::closeDevice called when " + "uninitialized, this should not happen"; + return; + } + auto pOpenedDevices = std::make_shared(*m_openedDevices.load()); VERIFY_OR_DEBUG_ASSERT(pOpenedDevices->contains(id)) { qWarning() << "device:" << id << "not opened"; diff --git a/src/soundio/pipewireenumerator.h b/src/soundio/pipewireenumerator.h index 63ce9f684a52..f8158bfd66ed 100644 --- a/src/soundio/pipewireenumerator.h +++ b/src/soundio/pipewireenumerator.h @@ -23,14 +23,11 @@ class PipewireEnumerator : public SoundDeviceEnumerator { QList getSampleRates() const override; std::vector queryDevices() const override; std::vector getAPIs() const override { - return std::vector{"PipeWire"}; + return m_initialized ? std::vector{"PipeWire"} : std::vector{}; } void initialize(); - pw_core* getCore(); - pw_thread_loop* getThreadLoop(); - bool isOpen(uint32_t id); void openDevice(uint32_t id, const std::set& inChans, diff --git a/src/soundio/soundmanager.cpp b/src/soundio/soundmanager.cpp index 940fc251aeda..c43aa94599c5 100644 --- a/src/soundio/soundmanager.cpp +++ b/src/soundio/soundmanager.cpp @@ -53,11 +53,11 @@ SoundManager::SoundManager(UserSettingsPointer pConfig, m_underflowUpdateCount(0), m_audioLatencyOverloadCount(kAppGroup, QStringLiteral("audio_latency_overload_count")), m_audioLatencyOverload(kAppGroup, QStringLiteral("audio_latency_overload")), - m_paEnumerator(std::make_unique(pConfig, this)), + m_pPaEnumerator(std::make_unique(pConfig, this)), #ifdef __PIPEWIRE__ - m_pipewireEnumerator(std::make_unique(pConfig, this)), + m_pPipewireEnumerator(std::make_unique(pConfig, this)), #endif - m_networkEnumerator(std::make_unique(pConfig, this)) { + m_pNetworkEnumerator(std::make_unique(pConfig, this)) { // TODO(xxx) some of these ControlObject are not needed by soundmanager, or are unused here. // It is possible to take them out? m_pControlObjectSoundStatusCO = new ControlObject( @@ -67,6 +67,10 @@ SoundManager::SoundManager(UserSettingsPointer pConfig, m_pControlObjectVinylControlGainCO = new ControlObject( ConfigKey(VINYL_PREF_KEY, "gain")); +#ifdef __PIPEWIRE__ + m_pPipewireEnumerator->initialize(); +#endif + queryDevices(); if (!m_config.readFromDisk()) { @@ -130,12 +134,12 @@ QList SoundManager::getDeviceList( QList SoundManager::getHostAPIList() const { QList apiList; - for (const auto& api : m_paEnumerator->getAPIs()) { + for (const auto& api : m_pPaEnumerator->getAPIs()) { apiList.push_back(api.c_str()); } #ifdef __PIPEWIRE__ - for (const auto& api : m_pipewireEnumerator->getAPIs()) { + for (const auto& api : m_pPipewireEnumerator->getAPIs()) { apiList.push_back(api.c_str()); } #endif @@ -231,7 +235,7 @@ void SoundManager::clearDeviceList(bool sleepAfterClosing) { m_devices.clear(); m_pErrorDevice.clear(); - m_paEnumerator->terminate(); + m_pPaEnumerator->terminate(); } QList SoundManager::getSampleRates(const QString& api) const { @@ -239,15 +243,15 @@ QList SoundManager::getSampleRates(const QString& api) if (api == MIXXX_PORTAUDIO_JACK_STRING) { // queryDevices must have been called for this to work, but the // ctor calls it -bkgood - samplerates = m_paEnumerator->getJackSampleRates(); + samplerates = m_pPaEnumerator->getJackSampleRates(); } #ifdef __PIPEWIRE__ else if (api == MIXXX_PIPEWIRE_STRING) { - samplerates = m_pipewireEnumerator->getSampleRates(); + samplerates = m_pPipewireEnumerator->getSampleRates(); } #endif else if (!api.isEmpty()) { - samplerates = m_paEnumerator->getSampleRates(); + samplerates = m_pPaEnumerator->getSampleRates(); } if (!samplerates.empty()) { @@ -269,21 +273,21 @@ void SoundManager::queryDevices() { qDebug() << "SoundManager::queryDevices()"; m_devices.clear(); - m_paEnumerator->initialize(); + m_pPaEnumerator->initialize(); - for (auto& device : m_paEnumerator->queryDevices()) { + for (auto& device : m_pPaEnumerator->queryDevices()) { m_devices.push_back(device); qDebug() << "m_devices.push_back " << device->getDisplayName(); } #ifdef __PIPEWIRE__ - for (auto& device : m_pipewireEnumerator->queryDevices()) { + for (auto& device : m_pPipewireEnumerator->queryDevices()) { m_devices.push_back(device); qDebug() << "m_devices.push_back " << device->getDisplayName(); } #endif - for (auto& device : m_networkEnumerator->queryDevices()) { + for (auto& device : m_pNetworkEnumerator->queryDevices()) { m_devices.push_back(device); qDebug() << "m_devices.push_back " << device->getDisplayName(); } diff --git a/src/soundio/soundmanager.h b/src/soundio/soundmanager.h index e61c4c696d3f..980f8022c9ab 100644 --- a/src/soundio/soundmanager.h +++ b/src/soundio/soundmanager.h @@ -89,7 +89,7 @@ class SoundManager : public QObject { QList registeredInputs() const; QSharedPointer getNetworkStream() const { - return m_networkEnumerator->getNetworkStream(); + return m_pNetworkEnumerator->getNetworkStream(); } void underflowHappened(int code) { @@ -165,11 +165,11 @@ class SoundManager : public QObject { PollingControlProxy m_audioLatencyOverloadCount; PollingControlProxy m_audioLatencyOverload; - std::unique_ptr m_paEnumerator; + std::unique_ptr m_pPaEnumerator; #ifdef __PIPEWIRE__ - std::unique_ptr m_pipewireEnumerator; + std::unique_ptr m_pPipewireEnumerator; #endif - std::unique_ptr m_networkEnumerator; + std::unique_ptr m_pNetworkEnumerator; }; From e4c555cef730ed40097b37d1c38500f9e067b5eb Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Mon, 22 Jun 2026 01:48:46 +0530 Subject: [PATCH 08/13] Improve pipewire members naming and deinitialization --- src/soundio/pipewireenumerator.cpp | 105 ++++++++++++++++++++--------- src/soundio/pipewireenumerator.h | 18 ++--- 2 files changed, 81 insertions(+), 42 deletions(-) diff --git a/src/soundio/pipewireenumerator.cpp b/src/soundio/pipewireenumerator.cpp index 9a959136dfc9..cf7812a552a2 100644 --- a/src/soundio/pipewireenumerator.cpp +++ b/src/soundio/pipewireenumerator.cpp @@ -68,6 +68,12 @@ static std::optional getPortIndexFromName(const char* name) { PipewireEnumerator::PipewireEnumerator(UserSettingsPointer, SoundManager* pManager) : m_pSoundManager(pManager), + m_pPwThreadLoop(nullptr), + m_pPwContext(nullptr), + m_pPwCore(nullptr), + m_pPwRegistry(nullptr), + m_pPwMetadata(nullptr), + m_pPwFilter(nullptr), m_soundDevices(std::make_shared()), m_openedDevices(std::make_shared()), m_initialized(false), @@ -77,34 +83,72 @@ PipewireEnumerator::PipewireEnumerator(UserSettingsPointer, SoundManager* pManag pw_init(nullptr, nullptr); - m_pThreadLoop = pw_thread_loop_new("mixxx_loop", nullptr); - m_pContext = pw_context_new(pw_thread_loop_get_loop(m_pThreadLoop), nullptr, 0); + m_pPwThreadLoop = pw_thread_loop_new("mixxx_loop", nullptr); + spa_zero(m_pwRegistryListener); + spa_zero(m_pwMetadataListener); + spa_zero(m_pwFilterListener); } PipewireEnumerator::~PipewireEnumerator() { - pw_thread_loop_stop(m_pThreadLoop); - spa_hook_remove(&m_registryListener); - spa_hook_remove(&m_metadataListener); - pw_proxy_destroy((struct pw_proxy*)m_pRegistry); - pw_proxy_destroy((struct pw_proxy*)m_pMetadata); - pw_core_disconnect(m_pCore); - pw_context_destroy(m_pContext); - pw_thread_loop_destroy(m_pThreadLoop); + pw_thread_loop_stop(m_pPwThreadLoop); + + if (m_pPwFilter) { + pw_filter_destroy(m_pPwFilter); + } + + if (m_pPwMetadata) { + spa_hook_remove(&m_pwMetadataListener); + pw_proxy_destroy((struct pw_proxy*)m_pPwMetadata); + } + + if (m_pPwRegistry) { + spa_hook_remove(&m_pwRegistryListener); + pw_proxy_destroy((struct pw_proxy*)m_pPwRegistry); + } + + if (m_pPwCore) { + pw_core_disconnect(m_pPwCore); + } + + if (m_pPwContext) { + pw_context_destroy(m_pPwContext); + } + + pw_thread_loop_destroy(m_pPwThreadLoop); pw_deinit(); } void PipewireEnumerator::initialize() { - m_pCore = pw_context_connect(m_pContext, nullptr, 0); + if (m_initialized) { + qWarning() << "PipewireEnumerator::initialize already initialized"; + return; + } - if (!m_pCore) { + if (!m_pPwContext) { + m_pPwContext = pw_context_new(pw_thread_loop_get_loop(m_pPwThreadLoop), nullptr, 0); + if (!m_pPwContext) { + qWarning() << "PipewireEnumerator::initialize pw_context_new " + "failed with error:" + << spa_strerror(errno); + return; + } + } + + m_pPwCore = pw_context_connect(m_pPwContext, nullptr, 0); + + if (!m_pPwCore) { + qWarning() << "PipewireEnumerator::initialize pw_context_connect " + "failed with error:" + << spa_strerror(errno); return; } - m_pRegistry = pw_core_get_registry(m_pCore, PW_VERSION_REGISTRY, 0); + m_pPwRegistry = pw_core_get_registry(m_pPwCore, PW_VERSION_REGISTRY, 0); + pw_registry_add_listener(m_pPwRegistry, &m_pwRegistryListener, ®istry_events, this); // see https://docs.pipewire.org/page_man_pipewire-props_7.html // and pipewire/keys.h header - m_pFilter = pw_filter_new(m_pCore, + m_pPwFilter = pw_filter_new(m_pPwCore, "mixxx", pw_properties_new(PW_KEY_MEDIA_NAME, "Mixxx", @@ -122,14 +166,9 @@ void PipewireEnumerator::initialize() { "Mixxx", nullptr)); - spa_zero(m_registryListener); - spa_zero(m_metadataListener); - spa_zero(m_filterListener); - - pw_registry_add_listener(m_pRegistry, &m_registryListener, ®istry_events, this); - pw_filter_add_listener(m_pFilter, &m_filterListener, &filter_events, this); + pw_filter_add_listener(m_pPwFilter, &m_pwFilterListener, &filter_events, this); - int res = pw_filter_connect(m_pFilter, + int res = pw_filter_connect(m_pPwFilter, PW_FILTER_FLAG_RT_PROCESS, nullptr, 0); @@ -138,7 +177,7 @@ void PipewireEnumerator::initialize() { qWarning() << "pw_filter_connect error:" << spa_strerror(res); } - pw_thread_loop_start(m_pThreadLoop); + pw_thread_loop_start(m_pPwThreadLoop); m_initialized = true; } @@ -158,13 +197,13 @@ void PipewireEnumerator::registryEventGlobal(uint32_t id, return; } - void* data = pw_registry_bind(m_pRegistry, + void* data = pw_registry_bind(m_pPwRegistry, id, PW_TYPE_INTERFACE_Metadata, PW_VERSION_METADATA, 0); - m_pMetadata = static_cast(data); - pw_metadata_add_listener(m_pMetadata, &m_metadataListener, &metadataEvents, this); + m_pPwMetadata = static_cast(data); + pw_metadata_add_listener(m_pPwMetadata, &m_pwMetadataListener, &metadataEvents, this); } else if (strcmp(pType, PW_TYPE_INTERFACE_Node) == 0) { const char* media_class = spa_dict_lookup(pProps, PW_KEY_MEDIA_CLASS); const char* media_type = spa_dict_lookup(pProps, PW_KEY_MEDIA_TYPE); @@ -346,7 +385,7 @@ void PipewireEnumerator::openDevice(uint32_t id, return; } - pw_thread_loop_lock(m_pThreadLoop); + pw_thread_loop_lock(m_pPwThreadLoop); if (rate != m_sampleRate.value() || framesPerBuffer != m_framesPerBuffer) { std::string rateStr = "1/" + std::to_string(rate); @@ -358,7 +397,7 @@ void PipewireEnumerator::openDevice(uint32_t id, }; spa_dict properties = SPA_DICT_INIT(items, 2); - int res = pw_filter_update_properties(m_pFilter, nullptr, &properties); + int res = pw_filter_update_properties(m_pPwFilter, nullptr, &properties); if (res >= 0) { m_sampleRate = mixxx::audio::SampleRate(rate); m_framesPerBuffer = framesPerBuffer; @@ -387,7 +426,7 @@ void PipewireEnumerator::openDevice(uint32_t id, "32 bit float mono audio", nullptr); pw_properties_setf(props, PW_KEY_PORT_NAME, "in:%zu", filterPortIndex); - void* port_data = pw_filter_add_port(m_pFilter, + void* port_data = pw_filter_add_port(m_ppwFilter, SPA_DIRECTION_INPUT, PW_FILTER_PORT_FLAG_MAP_BUFFERS, 0, @@ -404,7 +443,7 @@ void PipewireEnumerator::openDevice(uint32_t id, pw_properties* props = pw_properties_new( PW_KEY_FORMAT_DSP, "32 bit float mono audio", nullptr); pw_properties_setf(props, PW_KEY_PORT_NAME, "out:%zu", filterPortIndex); - void* port_data = pw_filter_add_port(m_pFilter, + void* port_data = pw_filter_add_port(m_ppwFilter, SPA_DIRECTION_OUTPUT, PW_FILTER_PORT_FLAG_MAP_BUFFERS, 0, @@ -413,7 +452,7 @@ void PipewireEnumerator::openDevice(uint32_t id, 0); outputs.emplace_back(port_data, i, filterPortIndex); } - pw_thread_loop_unlock(m_pThreadLoop); + pw_thread_loop_unlock(m_pPwThreadLoop); pOpenedDevices->emplace(id, Device{std::move(inputs), std::move(outputs)}); m_openedDevices.store(pOpenedDevices); @@ -434,7 +473,7 @@ void PipewireEnumerator::closeDevice(uint32_t id) { auto& device = pOpenedDevices->at(id); - pw_thread_loop_lock(m_pThreadLoop); + pw_thread_loop_lock(m_pPwThreadLoop); for (auto& port : device.inputs) { pw_filter_remove_port(port.pPortData); } @@ -442,7 +481,7 @@ void PipewireEnumerator::closeDevice(uint32_t id) { for (auto& port : device.outputs) { pw_filter_remove_port(port.pPortData); } - pw_thread_loop_unlock(m_pThreadLoop); + pw_thread_loop_unlock(m_pPwThreadLoop); pOpenedDevices->erase(id); m_openedDevices.store(pOpenedDevices); @@ -533,7 +572,7 @@ void PipewireEnumerator::createLink(uint32_t outNodeId, items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_LINK_INPUT_PORT, strInPort.c_str()); items[props.n_items++] = SPA_DICT_ITEM_INIT(PW_KEY_OBJECT_LINGER, "true"); - struct pw_proxy* pProxy = static_cast(pw_core_create_object(m_pCore, + struct pw_proxy* pProxy = static_cast(pw_core_create_object(m_pPwCore, "link-factory", PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, diff --git a/src/soundio/pipewireenumerator.h b/src/soundio/pipewireenumerator.h index f8158bfd66ed..73a9ddb8dbba 100644 --- a/src/soundio/pipewireenumerator.h +++ b/src/soundio/pipewireenumerator.h @@ -127,15 +127,15 @@ class PipewireEnumerator : public SoundDeviceEnumerator { SoundManager* m_pSoundManager; UserSettingsPointer m_pConfig; - pw_core* m_pCore; - pw_registry* m_pRegistry; - pw_context* m_pContext; - pw_metadata* m_pMetadata; - pw_thread_loop* m_pThreadLoop; - spa_hook m_registryListener; - spa_hook m_metadataListener; - spa_hook m_filterListener; - pw_filter* m_pFilter; + pw_thread_loop* m_pPwThreadLoop; + pw_context* m_pPwContext; + pw_core* m_pPwCore; + pw_registry* m_pPwRegistry; + pw_metadata* m_pPwMetadata; + pw_filter* m_pPwFilter; + spa_hook m_pwRegistryListener; + spa_hook m_pwFilterListener; + spa_hook m_pwMetadataListener; struct Device { struct Port { From 2e665d3d091fd7651f710f8eae19c9c25cc15319 Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Sun, 28 Jun 2026 18:06:06 +0530 Subject: [PATCH 09/13] Make Mixxx PipeWire ports static, get rid of atomic variables --- src/soundio/pipewireenumerator.cpp | 534 ++++++++++++++++++---------- src/soundio/pipewireenumerator.h | 52 ++- src/soundio/sounddevicepipewire.cpp | 79 ++-- src/soundio/sounddevicepipewire.h | 20 +- src/soundio/soundmanager.cpp | 4 - 5 files changed, 427 insertions(+), 262 deletions(-) diff --git a/src/soundio/pipewireenumerator.cpp b/src/soundio/pipewireenumerator.cpp index cf7812a552a2..a9db066a50ef 100644 --- a/src/soundio/pipewireenumerator.cpp +++ b/src/soundio/pipewireenumerator.cpp @@ -5,17 +5,21 @@ #include #include -#include +#include +#include +#include #include #include "audio/types.h" +#include "control/controlobject.h" #include "moc_pipewireenumerator.cpp" #include "soundio/sounddevice.h" #include "soundio/sounddevicepipewire.h" #include "soundio/soundmanager.h" +#include "soundio/soundmanagerutil.h" #include "util/assert.h" -#include "util/sample.h" #include "util/trace.h" +#include "util/types.h" #include "waveform/visualplayposition.h" namespace { @@ -39,31 +43,6 @@ static const char* find_node_name(const struct spa_dict* props) { } return nullptr; } - -// Calculates the port index from port name, since port name is very -// convenient property to access. That also mean that any changes in -// port naming scheme take in account this function. -static std::optional getPortIndexFromName(const char* name) { - std::string_view view(name); - - auto pos = view.find(':'); - if (pos == std::string_view::npos) { - return std::nullopt; - } - - uint32_t value; - auto [ptr, ec] = std::from_chars( - view.data() + pos + 1, - view.data() + view.size(), - value); - - if (ec == std::errc{}) { - return value; - } - - return std::nullopt; -} - } // namespace PipewireEnumerator::PipewireEnumerator(UserSettingsPointer, SoundManager* pManager) @@ -74,10 +53,19 @@ PipewireEnumerator::PipewireEnumerator(UserSettingsPointer, SoundManager* pManag m_pPwRegistry(nullptr), m_pPwMetadata(nullptr), m_pPwFilter(nullptr), - m_soundDevices(std::make_shared()), - m_openedDevices(std::make_shared()), m_initialized(false), - m_audioLatencyUsage(kAppGroup, QStringLiteral("audio_latency_usage")) { + m_sampleRate(48000), + m_audioLatencyUsage(kAppGroup, QStringLiteral("audio_latency_usage")), + m_framesPerBuffer(0) { + connect(m_pSoundManager, + &SoundManager::inputRegistered, + this, + &PipewireEnumerator::registerInput); + connect(m_pSoundManager, + &SoundManager::outputRegistered, + this, + &PipewireEnumerator::registerOutput); + connect(this, &PipewireEnumerator::deviceAdded, m_pSoundManager, &SoundManager::addDevice); connect(this, &PipewireEnumerator::deviceRemoved, m_pSoundManager, &SoundManager::removeDevice); @@ -87,6 +75,8 @@ PipewireEnumerator::PipewireEnumerator(UserSettingsPointer, SoundManager* pManag spa_zero(m_pwRegistryListener); spa_zero(m_pwMetadataListener); spa_zero(m_pwFilterListener); + + initialize(); } PipewireEnumerator::~PipewireEnumerator() { @@ -174,7 +164,8 @@ void PipewireEnumerator::initialize() { 0); VERIFY_OR_DEBUG_ASSERT(res >= 0) { - qWarning() << "pw_filter_connect error:" << spa_strerror(res); + qWarning() << "PipewireEnumerator::initialize pw_filter_connect error:" + << spa_strerror(res); } pw_thread_loop_start(m_pPwThreadLoop); @@ -221,59 +212,54 @@ void PipewireEnumerator::registryEventGlobal(uint32_t id, auto pDevice = QSharedPointer::create( m_pConfig, m_pSoundManager, this, id, name); emit deviceAdded(pDevice); - auto pSoundDevices = std::make_shared(*m_soundDevices.load()); - // pipewire assigns each object with a unique ID // any previous element is either invalid or already removed - pSoundDevices->insert_or_assign(id, std::move(pDevice)); - m_soundDevices.store(pSoundDevices); + m_soundDevices.insert_or_assign(id, std::move(pDevice)); + // this can be fooled if a different application names its node "Mixxx" if (strcmp(name, "Mixxx") == 0) { m_filterId = id; } } else if (strcmp(pType, PW_TYPE_INTERFACE_Port) == 0) { const uint32_t node_id = pw_properties_parse_int(spa_dict_lookup(pProps, PW_KEY_NODE_ID)); - const char* dir = spa_dict_lookup(pProps, PW_KEY_PORT_DIRECTION); - const bool isInput = strcmp(dir, "in") == 0; - - if (!m_soundDevices.load()->contains(node_id)) { + if (!m_soundDevices.contains(node_id)) { // most likely midi or video node return; } m_objects.insert_or_assign(id, Object{Port(node_id)}); - auto pSoundDevices = m_soundDevices.load(); - auto pSoundDevice = pSoundDevices->at(node_id); - pSoundDevice->registerDevicePort(id, pProps); + QSharedPointer pSoundDevice = m_soundDevices.at(node_id); + pSoundDevice->registerPort(id, pProps); m_pSoundManager->updateDeviceChannels(pSoundDevice); - if (node_id != m_filterId) { - return; - } - - auto portId = getPortIndexFromName(spa_dict_lookup(pProps, PW_KEY_PORT_NAME)); - VERIFY_OR_DEBUG_ASSERT(portId.has_value()) { - return; - } + const char* direction = spa_dict_lookup(pProps, PW_KEY_PORT_DIRECTION); - auto pOpenedDevices = *m_openedDevices.load(); + if (node_id == m_filterId) { + QString name(spa_dict_lookup(pProps, PW_KEY_PORT_NAME)); + QStringList list = name.split(':'); + if (strcmp(direction, "in") == 0) { + QList keys = m_inputs.keys(); + auto it = std::ranges::find(keys, list.at(0), &AudioPath::getString); + VERIFY_OR_DEBUG_ASSERT(it != keys.end()) { + return; + } - for (auto& [deviceId, device] : pOpenedDevices) { - if (isInput) { - for (auto& port : device.inputs) { - if (port.filterPort == portId) { - auto devicePorts = pSoundDevices->at(deviceId)->getOutPorts(); - uint32_t devicePortId = devicePorts[port.devicePort].id; - createLink(deviceId, devicePortId, node_id, id); - } + if (list.at(1) == "FL") { + *m_inputs.value(*it).first = id; + } else { + *m_inputs.value(*it).second = id; } } else { - for (auto& port : device.outputs) { - if (portId == port.filterPort) { - auto devicePorts = pSoundDevices->at(deviceId)->getInPorts(); - uint32_t devicePortId = devicePorts[port.devicePort].id; - createLink(node_id, id, deviceId, devicePortId); - } + QList keys = m_outputs.keys(); + auto it = std::ranges::find(keys, list.at(0), &AudioPath::getString); + VERIFY_OR_DEBUG_ASSERT(it != keys.end()) { + return; + } + + if (list.at(1) == "FL") { + *m_outputs.value(*it).first = id; + } else { + *m_outputs.value(*it).second = id; } } } @@ -287,8 +273,12 @@ void PipewireEnumerator::registryEventGlobal(uint32_t id, const uint32_t out_port = pw_properties_parse_int( spa_dict_lookup(pProps, PW_KEY_LINK_OUTPUT_PORT)); - if (in_node == m_filterId || out_node == m_filterId) { + if (in_node == m_filterId) { m_objects.insert_or_assign(id, Object{Link(in_port, out_port)}); + m_soundDevices.at(out_node)->registerLink(id, SPA_DIRECTION_OUTPUT); + } else if (out_node == m_filterId) { + m_objects.insert_or_assign(id, Object{Link(in_port, out_port)}); + m_soundDevices.at(in_node)->registerLink(id, SPA_DIRECTION_INPUT); } } } @@ -302,39 +292,41 @@ void PipewireEnumerator::registryEventGlobalRemove(unsigned int id) { Object& object = pair.mapped(); if (std::get_if(&object)) { - auto pSoundDevices = std::make_shared(*m_soundDevices.load()); - if (!pSoundDevices->contains(id)) { + if (!m_soundDevices.contains(id)) { return; } - auto pDevice = pSoundDevices->at(id); + QSharedPointer pDevice = m_soundDevices.at(id); if (pDevice->isOpen()) { pDevice->close(); } - qWarning() << "removing device:" << pDevice->getDisplayName(); - pSoundDevices->erase(id); - m_soundDevices.store(pSoundDevices); + m_soundDevices.erase(id); emit deviceRemoved(pDevice); // m_pSoundManager->removeDevice(device); - } else if (auto* port = std::get_if(&object)) { - auto pSoundDevices = m_soundDevices.load(); - VERIFY_OR_DEBUG_ASSERT(pSoundDevices->contains(port->nodeId)) { - qWarning() << "node" << port->nodeId << "port " << id; + } else if (Port* port = std::get_if(&object)) { + VERIFY_OR_DEBUG_ASSERT(m_soundDevices.contains(port->node)) { return; } - auto pSoundDevice = pSoundDevices->at(port->nodeId); - qWarning() << "removing port:" << id; - pSoundDevice->unregisterDevicePort(id); + QSharedPointer pSoundDevice = m_soundDevices.at(port->node); + pSoundDevice->unregisterPort(id); m_pSoundManager->updateDeviceChannels(pSoundDevice); + } else if (Link* link = std::get_if(&object)) { + Port input = std::get(m_objects.at(link->input)); + Port output = std::get(m_objects.at(link->output)); + + if (input.node == m_filterId) { + m_soundDevices.at(output.node)->unregisterLink(id, SPA_DIRECTION_OUTPUT); + } else if (output.node == m_filterId) { + m_soundDevices.at(input.node)->unregisterLink(id, SPA_DIRECTION_INPUT); + } } } std::vector PipewireEnumerator::queryDevices() const { std::vector devices; - auto pSoundDevices = m_soundDevices.load(); - for (const auto& [id, pDevice] : *pSoundDevices) { + for (const auto& [id, pDevice] : m_soundDevices) { devices.push_back(pDevice); } @@ -364,98 +356,92 @@ int PipewireEnumerator::metadataProperty( } bool PipewireEnumerator::isOpen(uint32_t id) { - return m_openedDevices.load()->contains(id); + return std::ranges::find(m_openedDevices, id) != m_openedDevices.end(); } -void PipewireEnumerator::openDevice(uint32_t id, - const std::set& inChans, - const std::set& outChans, - mixxx::audio::SampleRate rate, - uint32_t framesPerBuffer) { +std::string PipewireEnumerator::openDevice(const SoundDevicePipewire& device, + mixxx::audio::SampleRate sampleRate, + SINT framesPerBuffer) { + std::string result; VERIFY_OR_DEBUG_ASSERT(m_initialized) { qWarning() << "PipewireEnumerator::openDevice called when " "uninitialized, this should not happen"; - return; + return "PipewireEnumerator uninitialized"; } - auto pOpenedDevices = std::make_shared(*m_openedDevices.load()); + if (sampleRate != m_sampleRate || framesPerBuffer != m_framesPerBuffer) { + setLatency(sampleRate, framesPerBuffer); + } - VERIFY_OR_DEBUG_ASSERT(!pOpenedDevices->contains(id)) { - qWarning() << "device:" << id << "already open"; - return; + int deviceId = device.getDeviceId().deviceIndex; + + VERIFY_OR_DEBUG_ASSERT(std::ranges::find(m_openedDevices, deviceId) == m_openedDevices.end()) { + qWarning() << "SoundDevicePipewire:" << deviceId << "already open"; + return "Device already open"; } pw_thread_loop_lock(m_pPwThreadLoop); - if (rate != m_sampleRate.value() || framesPerBuffer != m_framesPerBuffer) { - std::string rateStr = "1/" + std::to_string(rate); - std::string latencyStr = std::to_string(framesPerBuffer) + "/" + std::to_string(rate); + // device.inputs() corresponds to output ports of device node + QList inKeys = m_inputs.keys(); + for (const AudioInputBuffer& input : device.inputs()) { + auto it = std::ranges::find_if(inKeys, [input](const AudioPath& path) { + return path.getType() == input.getType() && path.getIndex() == input.getIndex(); + }); - spa_dict_item items[] = { - SPA_DICT_ITEM_INIT(PW_KEY_NODE_RATE, rateStr.c_str()), - SPA_DICT_ITEM_INIT(PW_KEY_NODE_LATENCY, latencyStr.c_str()), - }; - spa_dict properties = SPA_DICT_INIT(items, 2); + VERIFY_OR_DEBUG_ASSERT(it != inKeys.end()) { + continue; + } + + std::pair filterPorts = m_inputs.value(*it); + ChannelGroup channelGroup = input.getChannelGroup(); + unsigned char channelBase = channelGroup.getChannelBase(); + unsigned char channelCount = channelGroup.getChannelCount().value(); + auto ports = device.getOutPorts(); - int res = pw_filter_update_properties(m_pPwFilter, nullptr, &properties); - if (res >= 0) { - m_sampleRate = mixxx::audio::SampleRate(rate); - m_framesPerBuffer = framesPerBuffer; + if (channelCount == 1) { + uint32_t filterPort = channelBase % 2 ? *filterPorts.second : *filterPorts.first; + result += createLink(deviceId, ports[channelBase].id, m_filterId, filterPort); } else { - qWarning() << "pw_filter_update_properties failed:" << spa_strerror(res); - qWarning() << "Unable to set requested samplerate and buffer size"; + result += createLink(deviceId, ports[channelBase].id, m_filterId, *filterPorts.first); + result += createLink(deviceId, + ports[channelBase + 1].id, + m_filterId, + *filterPorts.second); } } - size_t numInPorts = 0; - size_t numOutPorts = 0; - - for (auto& [id, device] : *pOpenedDevices) { - numInPorts += device.inputs.size(); - numOutPorts += device.outputs.size(); - } - - // these correspond to the AudioInputs and filter inputs - // and device outputs - std::vector inputs; - for (uint8_t i : inChans) { - size_t filterPortIndex = inputs.size() + numInPorts; - pw_properties* props = pw_properties_new( - // see pipewire/keys.h header - PW_KEY_FORMAT_DSP, - "32 bit float mono audio", - nullptr); - pw_properties_setf(props, PW_KEY_PORT_NAME, "in:%zu", filterPortIndex); - void* port_data = pw_filter_add_port(m_ppwFilter, - SPA_DIRECTION_INPUT, - PW_FILTER_PORT_FLAG_MAP_BUFFERS, - 0, - props, - nullptr, - 0); - inputs.emplace_back(port_data, i, filterPortIndex); - } - // these correspond to the AudioInputs and filter outputs - // and device inputs - std::vector outputs; - for (uint8_t i : outChans) { - size_t filterPortIndex = outputs.size() + numOutPorts; - pw_properties* props = pw_properties_new( - PW_KEY_FORMAT_DSP, "32 bit float mono audio", nullptr); - pw_properties_setf(props, PW_KEY_PORT_NAME, "out:%zu", filterPortIndex); - void* port_data = pw_filter_add_port(m_ppwFilter, - SPA_DIRECTION_OUTPUT, - PW_FILTER_PORT_FLAG_MAP_BUFFERS, - 0, - props, - nullptr, - 0); - outputs.emplace_back(port_data, i, filterPortIndex); + // device.outputs() corresponds to input ports of device node + QList outKeys = m_outputs.keys(); + for (const AudioOutputBuffer& output : device.outputs()) { + auto it = std::ranges::find_if(outKeys, [output](const AudioPath& path) { + return path.getType() == output.getType() && path.getIndex() == output.getIndex(); + }); + + VERIFY_OR_DEBUG_ASSERT(it != outKeys.end()) { + continue; + } + + std::pair filterPorts = m_outputs.value(*it); + ChannelGroup channelGroup = output.getChannelGroup(); + unsigned char channelBase = channelGroup.getChannelBase(); + unsigned char channelCount = channelGroup.getChannelCount().value(); + auto ports = device.getInPorts(); + + if (channelCount == 1) { + uint32_t filterPort = channelBase % 2 ? *filterPorts.second : *filterPorts.first; + result += createLink(m_filterId, filterPort, deviceId, ports[channelBase].id); + } else { + result += createLink(m_filterId, *filterPorts.first, deviceId, ports[channelBase].id); + result += createLink(m_filterId, + *filterPorts.second, + deviceId, + ports[channelBase + 1].id); + } } pw_thread_loop_unlock(m_pPwThreadLoop); - - pOpenedDevices->emplace(id, Device{std::move(inputs), std::move(outputs)}); - m_openedDevices.store(pOpenedDevices); + m_openedDevices.push_back(deviceId); + return result; } void PipewireEnumerator::closeDevice(uint32_t id) { @@ -465,26 +451,25 @@ void PipewireEnumerator::closeDevice(uint32_t id) { return; } - auto pOpenedDevices = std::make_shared(*m_openedDevices.load()); - VERIFY_OR_DEBUG_ASSERT(pOpenedDevices->contains(id)) { + auto deviceId = std::ranges::find(m_openedDevices, id); + + VERIFY_OR_DEBUG_ASSERT(deviceId != m_openedDevices.end()) { qWarning() << "device:" << id << "not opened"; return; } - auto& device = pOpenedDevices->at(id); + QSharedPointer pDevice = m_soundDevices.at(*deviceId); - pw_thread_loop_lock(m_pPwThreadLoop); - for (auto& port : device.inputs) { - pw_filter_remove_port(port.pPortData); + // device m_inLinks and m_outLinks are cleared by link registryEventGlobalRemove + for (uint32_t link : pDevice->getInLinks()) { + destroyLink(link); } - for (auto& port : device.outputs) { - pw_filter_remove_port(port.pPortData); + for (uint32_t link : pDevice->getOutLinks()) { + destroyLink(link); } - pw_thread_loop_unlock(m_pPwThreadLoop); - pOpenedDevices->erase(id); - m_openedDevices.store(pOpenedDevices); + m_openedDevices.erase(deviceId); } void PipewireEnumerator::callback(const spa_io_position* pos) { @@ -502,40 +487,88 @@ void PipewireEnumerator::callback(const spa_io_position* pos) { } #endif + const uint32_t sampleRate = pos->clock.rate.denom; const uint64_t framesPerBuffer = pos->clock.duration; + + if (sampleRate != m_sampleRate || framesPerBuffer != m_framesPerBuffer) { + qWarning() << "PipewireEnumerator::callback rate requested" + << m_sampleRate << "provided" << sampleRate + << "buffer size requested" << m_framesPerBuffer << "provided" + << framesPerBuffer; + setLatency(sampleRate, framesPerBuffer); + } + + qDebug() << "PipewireEnumerator::callback" << sampleRate << framesPerBuffer; m_pSoundManager->processUnderflowHappened(framesPerBuffer); - auto pOpenedDevices = m_openedDevices.load(); - auto pSoundDevices = m_soundDevices.load(); - - for (auto& [id, device] : *pOpenedDevices) { - auto pSoundDevice = pSoundDevices->at(id); - auto& ports = device.inputs; - for (const auto& port : ports) { - void* pBuffer = pw_filter_get_dsp_buffer(port.pPortData, framesPerBuffer); - pSoundDevice->writeInput(static_cast(pBuffer), - port.devicePort, - framesPerBuffer); + for (uint32_t deviceId : m_openedDevices) { + QSharedPointer device = m_soundDevices.at(deviceId); + QList deviceInputs = device->inputs(); + for (AudioInputBuffer input : deviceInputs) { + ChannelGroup channelGroup = input.getChannelGroup(); + const int iChannelCount = channelGroup.getChannelCount(); + const int iChannelBase = channelGroup.getChannelBase(); + CSAMPLE* pInputBuffer = input.getBuffer(); + + std::pair ports = m_inputs.value(input); + + const float* bufferFL = static_cast( + pw_filter_get_dsp_buffer(ports.first, framesPerBuffer)); + if (bufferFL) { + for (uint64_t i = 0; i < framesPerBuffer; i++) { + pInputBuffer[iChannelBase + i * iChannelCount] = bufferFL[i]; + } + } else { + for (uint64_t i = 0; i < framesPerBuffer; i++) { + pInputBuffer[iChannelBase + i * iChannelCount] = 0; + } + } + + const float* bufferFR = static_cast( + pw_filter_get_dsp_buffer(ports.second, framesPerBuffer)); + if (bufferFR) { + for (uint64_t i = 0; i < framesPerBuffer; i++) { + pInputBuffer[iChannelBase + 1 + i * iChannelCount] = bufferFR[i]; + } + } else { + for (uint64_t i = 0; i < framesPerBuffer; i++) { + pInputBuffer[iChannelBase + 1 + i * iChannelCount] = 0; + } + } } - m_pSoundManager->pushInputBuffers(pSoundDevice->inputs(), framesPerBuffer); + m_pSoundManager->pushInputBuffers(deviceInputs, framesPerBuffer); } m_pSoundManager->onDeviceOutputCallback(framesPerBuffer); - for (auto& [id, device] : *pOpenedDevices) { - auto pSoundDevice = pSoundDevices->at(id); - auto& ports = device.outputs; - for (const auto& port : ports) { - void* pBuffer = pw_filter_get_dsp_buffer(port.pPortData, framesPerBuffer); - if (!pBuffer) { - continue; + for (uint32_t deviceId : m_openedDevices) { + QSharedPointer device = m_soundDevices.at(deviceId); + for (AudioOutputBuffer output : device->outputs()) { + ChannelGroup chanGroup = output.getChannelGroup(); + const int iChannelCount = chanGroup.getChannelCount(); + const int iChannelBase = chanGroup.getChannelBase(); + const CSAMPLE* pOutputBuffer = output.getBuffer(); + + std::pair ports = m_outputs.value(output); + + float* bufferFL = static_cast( + pw_filter_get_dsp_buffer(ports.first, framesPerBuffer)); + if (bufferFL) { + for (uint64_t i = 0; i < framesPerBuffer; i++) { + bufferFL[i] = pOutputBuffer[iChannelBase + i * iChannelCount]; + } + } + + float* bufferFR = static_cast( + pw_filter_get_dsp_buffer(ports.second, framesPerBuffer)); + if (bufferFR) { + for (uint64_t i = 0; i < framesPerBuffer; i++) { + bufferFR[i] = pOutputBuffer[iChannelBase + 1 + i * iChannelCount]; + } } - SampleUtil::clear(static_cast(pBuffer), framesPerBuffer); - pSoundDevice->writeOutput(static_cast(pBuffer), - port.devicePort, - framesPerBuffer); } } + updateAudioLatencyUsage(framesPerBuffer); } @@ -554,10 +587,18 @@ void PipewireEnumerator::updateAudioLatencyUsage(const SINT framesPerBuffer) { m_timeInAudioCallback += m_clkRefTimer.elapsed(); } -void PipewireEnumerator::createLink(uint32_t outNodeId, +void PipewireEnumerator::destroyLink(uint32_t id) { + pw_thread_loop_lock(m_pPwThreadLoop); + pw_registry_destroy(m_pPwRegistry, id); + pw_thread_loop_unlock(m_pPwThreadLoop); +} + +std::string PipewireEnumerator::createLink(uint32_t outNodeId, uint32_t outPortId, uint32_t inNodeId, uint32_t inPortId) { + // qDebug() << "PipewireEnumerator::createLink" << outNodeId << outPortId << + // inNodeId << inPortId; spa_dict_item items[6]; spa_dict props = SPA_DICT_INIT(items, 0); @@ -580,9 +621,122 @@ void PipewireEnumerator::createLink(uint32_t outNodeId, 0)); if (pProxy) { pw_proxy_destroy(pProxy); + return {}; } + + return "createLink failed: outNodeId: " + + std::to_string(outNodeId) + + "outPortId: " + std::to_string(outPortId) + + "inNodeId: " + std::to_string(inNodeId) + + "inPortId: " + std::to_string(inPortId); } -mixxx::audio::SampleRate PipewireEnumerator::getDefaultSampleRate() const { - return m_defaultSampleRate; +void PipewireEnumerator::registerInput(const AudioInput& input, AudioDestination*) { + if (m_inputs.contains(input)) { + // duplicate VinylControl signal + return; + } + + pw_thread_loop_lock(m_pPwThreadLoop); + pw_properties* props = pw_properties_new( + // see pipewire/keys.h header + PW_KEY_FORMAT_DSP, + "32 bit float mono audio", + nullptr); + pw_properties_setf(props, PW_KEY_PORT_NAME, "%s:FL", input.getString().toStdString().c_str()); + void* portFL = pw_filter_add_port(m_pPwFilter, + SPA_DIRECTION_INPUT, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + sizeof(uint32_t), + props, + nullptr, + 0); + + props = pw_properties_new( + // see pipewire/keys.h header + PW_KEY_FORMAT_DSP, + "32 bit float mono audio", + nullptr); + pw_properties_setf(props, PW_KEY_PORT_NAME, "%s:FR", input.getString().toStdString().c_str()); + void* portFR = pw_filter_add_port(m_pPwFilter, + SPA_DIRECTION_INPUT, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + sizeof(uint32_t), + props, + nullptr, + 0); + pw_thread_loop_unlock(m_pPwThreadLoop); + m_inputs.insert(input, + std::pair{static_cast(portFL), + static_cast(portFR)}); +} + +void PipewireEnumerator::registerOutput(const AudioOutput& output, AudioSource*) { + pw_thread_loop_lock(m_pPwThreadLoop); + pw_properties* props = pw_properties_new( + // see pipewire/keys.h header + PW_KEY_FORMAT_DSP, + "32 bit float mono audio", + nullptr); + pw_properties_setf(props, PW_KEY_PORT_NAME, "%s:FL", output.getString().toStdString().c_str()); + void* portFL = pw_filter_add_port(m_pPwFilter, + SPA_DIRECTION_OUTPUT, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + sizeof(uint32_t), + props, + nullptr, + 0); + props = pw_properties_new( + // see pipewire/keys.h header + PW_KEY_FORMAT_DSP, + "32 bit float mono audio", + nullptr); + pw_properties_setf(props, PW_KEY_PORT_NAME, "%s:FR", output.getString().toStdString().c_str()); + void* portFR = pw_filter_add_port(m_pPwFilter, + SPA_DIRECTION_OUTPUT, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + sizeof(uint32_t), + props, + nullptr, + 0); + pw_thread_loop_unlock(m_pPwThreadLoop); + m_outputs.insert(output, + std::pair{static_cast(portFL), + static_cast(portFR)}); +} + +void PipewireEnumerator::setLatency(unsigned int sampleRate, unsigned int framesPerBuffer) { + qWarning() << "PipewireEnumerator::setLatency" << sampleRate << framesPerBuffer; + std::string rateStr = "1/" + std::to_string(sampleRate); + std::string latencyStr = std::to_string(framesPerBuffer) + "/" + std::to_string(sampleRate); + + spa_dict_item items[] = { + SPA_DICT_ITEM_INIT(PW_KEY_NODE_RATE, rateStr.c_str()), + SPA_DICT_ITEM_INIT(PW_KEY_NODE_LATENCY, latencyStr.c_str()), + }; + + // don't set PW_KEY_NODE_LATENCY if framesPerBuffer is 0 (uninitialized) + uint32_t numProps = framesPerBuffer == 0 ? 1 : 2; + spa_dict properties = SPA_DICT_INIT(items, numProps); + + pw_thread_loop_lock(m_pPwThreadLoop); + + int res = pw_filter_update_properties(m_pPwFilter, nullptr, &properties); + + pw_thread_loop_unlock(m_pPwThreadLoop); + + if (res >= 0) { + m_sampleRate = sampleRate; + m_framesPerBuffer = framesPerBuffer; + ControlObject::set( + ConfigKey(kAppGroup, QStringLiteral("output_latency_ms")), + m_framesPerBuffer * 1000 / m_sampleRate); + ControlObject::set(ConfigKey(kAppGroup, QStringLiteral("samplerate")), m_sampleRate); + + } else { + qWarning() << "PipewireEnumerator::setLatency " + "pw_filter_update_properties failed:" + << spa_strerror(res); + qWarning() << "Unable to set requested samplerate"; + } } diff --git a/src/soundio/pipewireenumerator.h b/src/soundio/pipewireenumerator.h index 73a9ddb8dbba..15d00612d135 100644 --- a/src/soundio/pipewireenumerator.h +++ b/src/soundio/pipewireenumerator.h @@ -5,8 +5,8 @@ #include #include -#include +#include "audio/types.h" #include "preferences/usersettings.h" #include "soundio/sounddevice.h" #include "soundio/sounddeviceenumerator.h" @@ -29,18 +29,22 @@ class PipewireEnumerator : public SoundDeviceEnumerator { void initialize(); bool isOpen(uint32_t id); - void openDevice(uint32_t id, - const std::set& inChans, - const std::set& outChans, - mixxx::audio::SampleRate rate, - uint32_t framesPerBuffer); + std::string openDevice(const SoundDevicePipewire& device, + mixxx::audio::SampleRate sampleRate, + SINT framesPerBuffer); void closeDevice(uint32_t id); - mixxx::audio::SampleRate getDefaultSampleRate() const; + mixxx::audio::SampleRate getDefaultSampleRate() const { + return m_defaultSampleRate; + } signals: void deviceAdded(SoundDevicePointer pDevice); void deviceRemoved(SoundDevicePointer pDevice); + private slots: + void registerInput(const AudioInput& input, AudioDestination* dest); + void registerOutput(const AudioOutput& output, AudioSource* src); + private: static void registryEventGlobalOuter(void* data, uint32_t id, @@ -103,11 +107,14 @@ class PipewireEnumerator : public SoundDeviceEnumerator { void writeInput(const float* input, int channel, int framesPerBuffer, int offset = 0); void writeOutput(float* output, int channel, int framesPerBuffer, int offset = 0); - void createLink(uint32_t outNodeId, + std::string createLink(uint32_t outNodeId, uint32_t outPortId, uint32_t inNodeI, uint32_t inPortId); + void destroyLink(uint32_t id); + void updateAudioLatencyUsage(const SINT framesPerBuffer); + void setLatency(unsigned int sampleRate, unsigned int framesPerBuffer); struct Link { uint32_t input; @@ -115,7 +122,7 @@ class PipewireEnumerator : public SoundDeviceEnumerator { }; struct Port { - uint32_t nodeId; + uint32_t node; }; struct Node {}; @@ -137,27 +144,8 @@ class PipewireEnumerator : public SoundDeviceEnumerator { spa_hook m_pwFilterListener; spa_hook m_pwMetadataListener; - struct Device { - struct Port { - void* pPortData; - - // these are our own indices, and are not assigned by - // pipewire in any way - uint32_t devicePort; - uint32_t filterPort; - }; - - // inputs correspond to filter inputs and soundDevice outputs - std::vector inputs; - // outputs correspond to filter outputs and soundDevice inputs - std::vector outputs; - }; - - using SoundDeviceMap = std::unordered_map>; - std::atomic> m_soundDevices; - - using DeviceMap = std::unordered_map; - std::atomic> m_openedDevices; + std::unordered_map> m_soundDevices; + std::vector m_openedDevices; bool m_initialized; uint64_t xrun_duration; @@ -167,10 +155,12 @@ class PipewireEnumerator : public SoundDeviceEnumerator { mixxx::audio::SampleRate m_sampleRate; mixxx::audio::SampleRate m_defaultSampleRate; - uint32_t m_framesPerBuffer; + QHash> m_inputs; + QHash> m_outputs; PollingControlProxy m_audioLatencyUsage; mixxx::Duration m_timeInAudioCallback; int m_framesSinceAudioLatencyUsageUpdate; uint32_t m_filterId; + uint32_t m_framesPerBuffer; }; diff --git a/src/soundio/sounddevicepipewire.cpp b/src/soundio/sounddevicepipewire.cpp index a7d0ab2de24d..c6561bbded17 100644 --- a/src/soundio/sounddevicepipewire.cpp +++ b/src/soundio/sounddevicepipewire.cpp @@ -4,7 +4,9 @@ #include "soundio/pipewireenumerator.h" #include "soundio/sounddevice.h" +#include "soundio/sounddevicestatus.h" #include "soundio/soundmanagerconfig.h" +#include "soundio/soundmanagerutil.h" #include "util/sample.h" SoundDevicePipewire::SoundDevicePipewire(UserSettingsPointer pConfig, @@ -14,7 +16,7 @@ SoundDevicePipewire::SoundDevicePipewire(UserSettingsPointer pConfig, const std::string_view name) : SoundDevice(pConfig, pManager), m_pEnumerator(pEnumerator) { - m_hostAPI = "PipeWire"; + m_hostAPI = QStringLiteral("PipeWire"); m_deviceId.name = name.data(); m_deviceId.deviceIndex = id; m_strDisplayName = QString::fromUtf8(name); @@ -24,30 +26,12 @@ SoundDevicePipewire::SoundDevicePipewire(UserSettingsPointer pConfig, } SoundDeviceStatus SoundDevicePipewire::open(bool, int) { - std::set inChans; - for (auto& input : m_audioInputs) { - ChannelGroup channelGroup = input.getChannelGroup(); - uint8_t highChannel = channelGroup.getChannelBase() + channelGroup.getChannelCount(); - for (uint8_t i = channelGroup.getChannelBase(); i < highChannel; i++) { - inChans.insert(i); - } - } - - std::set outChans; - for (auto& output : m_audioOutputs) { - ChannelGroup channelGroup = output.getChannelGroup(); - uint8_t highChannel = channelGroup.getChannelBase() + channelGroup.getChannelCount(); - for (uint8_t i = channelGroup.getChannelBase(); i < highChannel; i++) { - outChans.insert(i); - } + m_error = m_pEnumerator->openDevice(*this, m_sampleRate, m_configFramesPerBuffer); + if (m_error.empty()) { + return SoundDeviceStatus::Ok; + } else { + return SoundDeviceStatus::Error; } - - m_pEnumerator->openDevice(m_deviceId.deviceIndex, - inChans, - outChans, - m_sampleRate, - m_configFramesPerBuffer); - return SoundDeviceStatus::Ok; } bool SoundDevicePipewire::isOpen() const { @@ -123,30 +107,35 @@ void SoundDevicePipewire::writeInput( } } -void SoundDevicePipewire::registerDevicePort(uint32_t id, const struct spa_dict* props) { - std::string name = spa_dict_lookup(props, PW_KEY_PORT_ALIAS); - const char* dir = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION); +void SoundDevicePipewire::registerPort(uint32_t id, const struct spa_dict* props) { + const char* nameStr = spa_dict_lookup(props, PW_KEY_PORT_ALIAS); + const char* direction = spa_dict_lookup(props, PW_KEY_PORT_DIRECTION); + std::string name; - if (name.empty()) { - name = spa_dict_lookup(props, PW_KEY_PORT_NAME); + if (!nameStr) { + nameStr = spa_dict_lookup(props, PW_KEY_PORT_NAME); } - if (name.empty()) { - name = dir; + if (nameStr) { + name = nameStr; + } else { + name = direction; name += ":"; name += spa_dict_lookup(props, PW_KEY_PORT_ID); } - if (strcmp(dir, "in") == 0) { + // m_numInputChannels, m_numOutputChannels, m_audioInputs, m_audioOutputs + // are with respect to Mixxx and not the SoundDevice + if (strcmp(direction, "in") == 0) { m_inPorts.emplace_back(id, name); m_numOutputChannels = mixxx::audio::ChannelCount::fromInt(m_inPorts.size()); - } else if (strcmp(dir, "out") == 0) { + } else if (strcmp(direction, "out") == 0) { m_outPorts.emplace_back(id, name); m_numInputChannels = mixxx::audio::ChannelCount::fromInt(m_outPorts.size()); } } -void SoundDevicePipewire::unregisterDevicePort(uint32_t id) { +void SoundDevicePipewire::unregisterPort(uint32_t id) { for (auto it = m_inPorts.begin(); it != m_inPorts.end(); it++) { if (it->id == id) { m_inPorts.erase(it); @@ -169,3 +158,25 @@ mixxx::audio::SampleRate SoundDevicePipewire::getDefaultSampleRate() const { return SoundManagerConfig::kMixxxDefaultSampleRate; } + +void SoundDevicePipewire::registerLink(uint32_t id, spa_direction direction) { + if (direction == SPA_DIRECTION_INPUT) { + m_inLinks.push_back(id); + } else { + m_outLinks.push_back(id); + } +} + +void SoundDevicePipewire::unregisterLink(uint32_t id, spa_direction direction) { + if (direction == SPA_DIRECTION_INPUT) { + auto it = std::ranges::find(m_inLinks, id); + if (it != m_inLinks.end()) { + m_inLinks.erase(it); + } + } else { + auto it = std::ranges::find(m_outLinks, id); + if (it != m_outLinks.end()) { + m_outLinks.erase(it); + } + } +} diff --git a/src/soundio/sounddevicepipewire.h b/src/soundio/sounddevicepipewire.h index 1e73f7d37639..0d6cb30acb5e 100644 --- a/src/soundio/sounddevicepipewire.h +++ b/src/soundio/sounddevicepipewire.h @@ -24,7 +24,7 @@ class SoundDevicePipewire : public SoundDevice { void writeProcess(SINT) override { } QString getError() const override { - return {}; + return m_error.c_str(); } mixxx::audio::SampleRate getDefaultSampleRate() const override; @@ -36,8 +36,18 @@ class SoundDevicePipewire : public SoundDevice { uint32_t outPortId, uint32_t inNodeId, uint32_t inPortId); - void registerDevicePort(uint32_t id, const struct spa_dict* props); - void unregisterDevicePort(uint32_t id); + void registerPort(uint32_t id, const struct spa_dict* props); + void unregisterPort(uint32_t id); + void registerLink(uint32_t id, spa_direction direction); + void unregisterLink(uint32_t id, spa_direction direction); + + std::span getInLinks() const { + return m_inLinks; + } + + std::span getOutLinks() const { + return m_outLinks; + } struct Port { uint32_t id; @@ -56,4 +66,8 @@ class SoundDevicePipewire : public SoundDevice { PipewireEnumerator* m_pEnumerator; std::vector m_inPorts; std::vector m_outPorts; + + std::vector m_inLinks; + std::vector m_outLinks; + std::string m_error; }; diff --git a/src/soundio/soundmanager.cpp b/src/soundio/soundmanager.cpp index c43aa94599c5..6b1872e43cd5 100644 --- a/src/soundio/soundmanager.cpp +++ b/src/soundio/soundmanager.cpp @@ -67,10 +67,6 @@ SoundManager::SoundManager(UserSettingsPointer pConfig, m_pControlObjectVinylControlGainCO = new ControlObject( ConfigKey(VINYL_PREF_KEY, "gain")); -#ifdef __PIPEWIRE__ - m_pPipewireEnumerator->initialize(); -#endif - queryDevices(); if (!m_config.readFromDisk()) { From 8ac7d56c9eba38a468177978f3da1fed92e8119f Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Tue, 30 Jun 2026 02:57:54 +0530 Subject: [PATCH 10/13] Safeguard port creation when PipeWire server not connected --- src/soundio/pipewireenumerator.cpp | 132 ++++++++++++++++------------- src/soundio/pipewireenumerator.h | 3 + 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/src/soundio/pipewireenumerator.cpp b/src/soundio/pipewireenumerator.cpp index a9db066a50ef..b499b5d31ad4 100644 --- a/src/soundio/pipewireenumerator.cpp +++ b/src/soundio/pipewireenumerator.cpp @@ -83,6 +83,7 @@ PipewireEnumerator::~PipewireEnumerator() { pw_thread_loop_stop(m_pPwThreadLoop); if (m_pPwFilter) { + spa_hook_remove(&m_pwFilterListener); pw_filter_destroy(m_pPwFilter); } @@ -110,7 +111,6 @@ PipewireEnumerator::~PipewireEnumerator() { void PipewireEnumerator::initialize() { if (m_initialized) { - qWarning() << "PipewireEnumerator::initialize already initialized"; return; } @@ -158,6 +158,16 @@ void PipewireEnumerator::initialize() { pw_filter_add_listener(m_pPwFilter, &m_pwFilterListener, &filter_events, this); + const auto registeredOutputs = m_pSoundManager->registeredOutputs(); + for (const auto& output : registeredOutputs) { + createOutputPorts(output); + } + + const auto registeredInputs = m_pSoundManager->registeredInputs(); + for (const auto& input : registeredInputs) { + createInputPorts(input); + } + int res = pw_filter_connect(m_pPwFilter, PW_FILTER_FLAG_RT_PROCESS, nullptr, @@ -491,10 +501,11 @@ void PipewireEnumerator::callback(const spa_io_position* pos) { const uint64_t framesPerBuffer = pos->clock.duration; if (sampleRate != m_sampleRate || framesPerBuffer != m_framesPerBuffer) { - qWarning() << "PipewireEnumerator::callback rate requested" - << m_sampleRate << "provided" << sampleRate - << "buffer size requested" << m_framesPerBuffer << "provided" - << framesPerBuffer; + qWarning() << "PipewireEnumerator::callback" + "requested" + << m_framesPerBuffer << "samples at" << m_sampleRate << "hz," + "provided" + << framesPerBuffer << "samples at" << sampleRate << "hz"; setLatency(sampleRate, framesPerBuffer); } @@ -504,7 +515,7 @@ void PipewireEnumerator::callback(const spa_io_position* pos) { for (uint32_t deviceId : m_openedDevices) { QSharedPointer device = m_soundDevices.at(deviceId); QList deviceInputs = device->inputs(); - for (AudioInputBuffer input : deviceInputs) { + for (const AudioInputBuffer& input : deviceInputs) { ChannelGroup channelGroup = input.getChannelGroup(); const int iChannelCount = channelGroup.getChannelCount(); const int iChannelBase = channelGroup.getChannelBase(); @@ -543,7 +554,7 @@ void PipewireEnumerator::callback(const spa_io_position* pos) { for (uint32_t deviceId : m_openedDevices) { QSharedPointer device = m_soundDevices.at(deviceId); - for (AudioOutputBuffer output : device->outputs()) { + for (const AudioOutputBuffer& output : device->outputs()) { ChannelGroup chanGroup = output.getChannelGroup(); const int iChannelCount = chanGroup.getChannelCount(); const int iChannelBase = chanGroup.getChannelBase(); @@ -637,72 +648,71 @@ void PipewireEnumerator::registerInput(const AudioInput& input, AudioDestination return; } - pw_thread_loop_lock(m_pPwThreadLoop); - pw_properties* props = pw_properties_new( - // see pipewire/keys.h header - PW_KEY_FORMAT_DSP, - "32 bit float mono audio", - nullptr); - pw_properties_setf(props, PW_KEY_PORT_NAME, "%s:FL", input.getString().toStdString().c_str()); - void* portFL = pw_filter_add_port(m_pPwFilter, - SPA_DIRECTION_INPUT, - PW_FILTER_PORT_FLAG_MAP_BUFFERS, - sizeof(uint32_t), - props, - nullptr, - 0); - - props = pw_properties_new( - // see pipewire/keys.h header - PW_KEY_FORMAT_DSP, - "32 bit float mono audio", - nullptr); - pw_properties_setf(props, PW_KEY_PORT_NAME, "%s:FR", input.getString().toStdString().c_str()); - void* portFR = pw_filter_add_port(m_pPwFilter, - SPA_DIRECTION_INPUT, - PW_FILTER_PORT_FLAG_MAP_BUFFERS, - sizeof(uint32_t), - props, - nullptr, - 0); - pw_thread_loop_unlock(m_pPwThreadLoop); - m_inputs.insert(input, - std::pair{static_cast(portFL), - static_cast(portFR)}); + if (m_initialized) { + pw_thread_loop_lock(m_pPwThreadLoop); + createInputPorts(input); + pw_thread_loop_unlock(m_pPwThreadLoop); + } } void PipewireEnumerator::registerOutput(const AudioOutput& output, AudioSource*) { - pw_thread_loop_lock(m_pPwThreadLoop); + if (m_initialized) { + pw_thread_loop_lock(m_pPwThreadLoop); + createOutputPorts(output); + pw_thread_loop_unlock(m_pPwThreadLoop); + } +} + +// need to pw_thread_loop_lock before calling this +uint32_t* PipewireEnumerator::createPorts(const AudioPath& path, bool channel) { + spa_direction direction; + switch (path.getType()) { + case AudioPathType::Main: + case AudioPathType::Headphones: + case AudioPathType::Booth: + case AudioPathType::Bus: + case AudioPathType::Deck: + direction = SPA_DIRECTION_OUTPUT; + break; + case AudioPathType::VinylControl: + case AudioPathType::Microphone: + case AudioPathType::Auxiliary: + case AudioPathType::RecordBroadcast: + direction = SPA_DIRECTION_INPUT; + break; + default: + qWarning() << "PipewireEnumerator::createPorts path type AudioPathType::Invalid"; + return nullptr; + } + pw_properties* props = pw_properties_new( // see pipewire/keys.h header PW_KEY_FORMAT_DSP, "32 bit float mono audio", nullptr); - pw_properties_setf(props, PW_KEY_PORT_NAME, "%s:FL", output.getString().toStdString().c_str()); - void* portFL = pw_filter_add_port(m_pPwFilter, - SPA_DIRECTION_OUTPUT, + pw_properties_setf(props, + PW_KEY_PORT_NAME, + channel ? "%s:FR" : "%s:FL", + path.getString().toStdString().c_str()); + return static_cast(pw_filter_add_port(m_pPwFilter, + direction, PW_FILTER_PORT_FLAG_MAP_BUFFERS, sizeof(uint32_t), props, nullptr, - 0); - props = pw_properties_new( - // see pipewire/keys.h header - PW_KEY_FORMAT_DSP, - "32 bit float mono audio", - nullptr); - pw_properties_setf(props, PW_KEY_PORT_NAME, "%s:FR", output.getString().toStdString().c_str()); - void* portFR = pw_filter_add_port(m_pPwFilter, - SPA_DIRECTION_OUTPUT, - PW_FILTER_PORT_FLAG_MAP_BUFFERS, - sizeof(uint32_t), - props, - nullptr, - 0); - pw_thread_loop_unlock(m_pPwThreadLoop); - m_outputs.insert(output, - std::pair{static_cast(portFL), - static_cast(portFR)}); + 0)); +} + +// need to pw_thread_loop_lock before calling this +void PipewireEnumerator::createInputPorts(const AudioInput& input) { + auto ports = std::pair{createPorts(input, false), createPorts(input, true)}; + m_inputs.insert(input, ports); +} + +// need to pw_thread_loop_lock before calling this +void PipewireEnumerator::createOutputPorts(const AudioOutput& output) { + auto ports = std::pair{createPorts(output, false), createPorts(output, true)}; + m_outputs.insert(output, ports); } void PipewireEnumerator::setLatency(unsigned int sampleRate, unsigned int framesPerBuffer) { diff --git a/src/soundio/pipewireenumerator.h b/src/soundio/pipewireenumerator.h index 15d00612d135..c1fb2bd0512f 100644 --- a/src/soundio/pipewireenumerator.h +++ b/src/soundio/pipewireenumerator.h @@ -115,6 +115,9 @@ class PipewireEnumerator : public SoundDeviceEnumerator { void updateAudioLatencyUsage(const SINT framesPerBuffer); void setLatency(unsigned int sampleRate, unsigned int framesPerBuffer); + void createInputPorts(const AudioInput& path); + void createOutputPorts(const AudioOutput& path); + uint32_t* createPorts(const AudioPath& path, bool channel); struct Link { uint32_t input; From 03231842fbd0c24c834d8d78d911a0fdb3a175d0 Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Sat, 4 Jul 2026 04:13:40 +0530 Subject: [PATCH 11/13] Connect single SoundDevice port to Mixxx port and internally make channel stereo --- src/soundio/pipewireenumerator.cpp | 71 ++++++++++++++++++------------ 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/src/soundio/pipewireenumerator.cpp b/src/soundio/pipewireenumerator.cpp index b499b5d31ad4..9172f8cddc72 100644 --- a/src/soundio/pipewireenumerator.cpp +++ b/src/soundio/pipewireenumerator.cpp @@ -523,27 +523,31 @@ void PipewireEnumerator::callback(const spa_io_position* pos) { std::pair ports = m_inputs.value(input); - const float* bufferFL = static_cast( - pw_filter_get_dsp_buffer(ports.first, framesPerBuffer)); - if (bufferFL) { - for (uint64_t i = 0; i < framesPerBuffer; i++) { - pInputBuffer[iChannelBase + i * iChannelCount] = bufferFL[i]; + if (iChannelCount == 1) { + void* portData = iChannelBase % 2 ? ports.second : ports.first; + const float* buffer = static_cast( + pw_filter_get_dsp_buffer(portData, framesPerBuffer)); + if (buffer) { + for (uint64_t i = 0; i < framesPerBuffer; i++) { + pInputBuffer[i * 2] = buffer[i]; + pInputBuffer[i * 2 + 1] = buffer[i]; + } } } else { - for (uint64_t i = 0; i < framesPerBuffer; i++) { - pInputBuffer[iChannelBase + i * iChannelCount] = 0; + const float* bufferFL = static_cast( + pw_filter_get_dsp_buffer(ports.first, framesPerBuffer)); + if (bufferFL) { + for (uint64_t i = 0; i < framesPerBuffer; i++) { + pInputBuffer[iChannelBase + i * 2] = bufferFL[i]; + } } - } - const float* bufferFR = static_cast( - pw_filter_get_dsp_buffer(ports.second, framesPerBuffer)); - if (bufferFR) { - for (uint64_t i = 0; i < framesPerBuffer; i++) { - pInputBuffer[iChannelBase + 1 + i * iChannelCount] = bufferFR[i]; - } - } else { - for (uint64_t i = 0; i < framesPerBuffer; i++) { - pInputBuffer[iChannelBase + 1 + i * iChannelCount] = 0; + const float* bufferFR = static_cast( + pw_filter_get_dsp_buffer(ports.second, framesPerBuffer)); + if (bufferFR) { + for (uint64_t i = 0; i < framesPerBuffer; i++) { + pInputBuffer[iChannelBase + 1 + i * 2] = bufferFR[i]; + } } } } @@ -562,19 +566,30 @@ void PipewireEnumerator::callback(const spa_io_position* pos) { std::pair ports = m_outputs.value(output); - float* bufferFL = static_cast( - pw_filter_get_dsp_buffer(ports.first, framesPerBuffer)); - if (bufferFL) { - for (uint64_t i = 0; i < framesPerBuffer; i++) { - bufferFL[i] = pOutputBuffer[iChannelBase + i * iChannelCount]; + if (iChannelCount == 1) { + void* portData = iChannelBase % 2 ? ports.second : ports.first; + float* buffer = static_cast( + pw_filter_get_dsp_buffer(portData, framesPerBuffer)); + if (buffer) { + for (uint64_t i = 0; i < framesPerBuffer; i++) { + buffer[i] = pOutputBuffer[iChannelBase + i * 2]; + } + } + } else { + float* bufferFL = static_cast( + pw_filter_get_dsp_buffer(ports.first, framesPerBuffer)); + if (bufferFL) { + for (uint64_t i = 0; i < framesPerBuffer; i++) { + bufferFL[i] = pOutputBuffer[iChannelBase + i * 2]; + } } - } - float* bufferFR = static_cast( - pw_filter_get_dsp_buffer(ports.second, framesPerBuffer)); - if (bufferFR) { - for (uint64_t i = 0; i < framesPerBuffer; i++) { - bufferFR[i] = pOutputBuffer[iChannelBase + 1 + i * iChannelCount]; + float* bufferFR = static_cast( + pw_filter_get_dsp_buffer(ports.second, framesPerBuffer)); + if (bufferFR) { + for (uint64_t i = 0; i < framesPerBuffer; i++) { + bufferFR[i] = pOutputBuffer[iChannelBase + 1 + i * 2]; + } } } } From 276f17195a03330f1f4ec83384baea50c3f86c03 Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Wed, 8 Jul 2026 06:21:10 +0530 Subject: [PATCH 12/13] Check for hidden input/output before creating PipeWire ports --- src/soundio/pipewireenumerator.cpp | 85 +++++++++++++++--------------- src/soundio/pipewireenumerator.h | 6 +-- 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/src/soundio/pipewireenumerator.cpp b/src/soundio/pipewireenumerator.cpp index 9172f8cddc72..8e91a685da55 100644 --- a/src/soundio/pipewireenumerator.cpp +++ b/src/soundio/pipewireenumerator.cpp @@ -158,14 +158,12 @@ void PipewireEnumerator::initialize() { pw_filter_add_listener(m_pPwFilter, &m_pwFilterListener, &filter_events, this); - const auto registeredOutputs = m_pSoundManager->registeredOutputs(); - for (const auto& output : registeredOutputs) { - createOutputPorts(output); + for (auto it = m_inputs.begin(); it != m_inputs.end(); ++it) { + it.value() = createInputPorts(it.key()); } - const auto registeredInputs = m_pSoundManager->registeredInputs(); - for (const auto& input : registeredInputs) { - createInputPorts(input); + for (auto it = m_outputs.begin(); it != m_outputs.end(); ++it) { + it.value() = createOutputPorts(it.key()); } int res = pw_filter_connect(m_pPwFilter, @@ -658,76 +656,79 @@ std::string PipewireEnumerator::createLink(uint32_t outNodeId, } void PipewireEnumerator::registerInput(const AudioInput& input, AudioDestination*) { - if (m_inputs.contains(input)) { + if (m_inputs.contains(input) or input.isHidden()) { // duplicate VinylControl signal return; } if (m_initialized) { pw_thread_loop_lock(m_pPwThreadLoop); - createInputPorts(input); + m_inputs.insert(input, createInputPorts(input)); pw_thread_loop_unlock(m_pPwThreadLoop); + } else { + m_inputs.insert(input, {}); } } void PipewireEnumerator::registerOutput(const AudioOutput& output, AudioSource*) { + if (output.isHidden()) { + return; + } + if (m_initialized) { pw_thread_loop_lock(m_pPwThreadLoop); - createOutputPorts(output); + m_outputs.insert(output, createOutputPorts(output)); pw_thread_loop_unlock(m_pPwThreadLoop); + } else { + m_outputs.insert(output, {}); } } // need to pw_thread_loop_lock before calling this -uint32_t* PipewireEnumerator::createPorts(const AudioPath& path, bool channel) { - spa_direction direction; - switch (path.getType()) { - case AudioPathType::Main: - case AudioPathType::Headphones: - case AudioPathType::Booth: - case AudioPathType::Bus: - case AudioPathType::Deck: - direction = SPA_DIRECTION_OUTPUT; - break; - case AudioPathType::VinylControl: - case AudioPathType::Microphone: - case AudioPathType::Auxiliary: - case AudioPathType::RecordBroadcast: - direction = SPA_DIRECTION_INPUT; - break; - default: - qWarning() << "PipewireEnumerator::createPorts path type AudioPathType::Invalid"; - return nullptr; - } - +std::pair PipewireEnumerator::createPorts( + std::string_view name, spa_direction direction) { pw_properties* props = pw_properties_new( // see pipewire/keys.h header PW_KEY_FORMAT_DSP, "32 bit float mono audio", nullptr); - pw_properties_setf(props, - PW_KEY_PORT_NAME, - channel ? "%s:FR" : "%s:FL", - path.getString().toStdString().c_str()); - return static_cast(pw_filter_add_port(m_pPwFilter, + pw_properties_setf(props, PW_KEY_PORT_NAME, "%s:FL", name.data()); + + void* leftPort = pw_filter_add_port(m_pPwFilter, direction, PW_FILTER_PORT_FLAG_MAP_BUFFERS, sizeof(uint32_t), props, nullptr, - 0)); + 0); + + props = pw_properties_new( + // see pipewire/keys.h header + PW_KEY_FORMAT_DSP, + "32 bit float mono audio", + nullptr); + pw_properties_setf(props, PW_KEY_PORT_NAME, "%s:FR", name.data()); + + void* rightPort = pw_filter_add_port(m_pPwFilter, + direction, + PW_FILTER_PORT_FLAG_MAP_BUFFERS, + sizeof(uint32_t), + props, + nullptr, + 0); + return std::pair{static_cast(leftPort), static_cast(rightPort)}; } // need to pw_thread_loop_lock before calling this -void PipewireEnumerator::createInputPorts(const AudioInput& input) { - auto ports = std::pair{createPorts(input, false), createPorts(input, true)}; - m_inputs.insert(input, ports); +std::pair PipewireEnumerator::createInputPorts(const AudioInput& input) { + std::string inputName = input.getString().toStdString(); + return createPorts(inputName, SPA_DIRECTION_INPUT); } // need to pw_thread_loop_lock before calling this -void PipewireEnumerator::createOutputPorts(const AudioOutput& output) { - auto ports = std::pair{createPorts(output, false), createPorts(output, true)}; - m_outputs.insert(output, ports); +std::pair PipewireEnumerator::createOutputPorts(const AudioOutput& output) { + std::string outputName = output.getString().toStdString(); + return createPorts(outputName, SPA_DIRECTION_OUTPUT); } void PipewireEnumerator::setLatency(unsigned int sampleRate, unsigned int framesPerBuffer) { diff --git a/src/soundio/pipewireenumerator.h b/src/soundio/pipewireenumerator.h index c1fb2bd0512f..c8478bab164b 100644 --- a/src/soundio/pipewireenumerator.h +++ b/src/soundio/pipewireenumerator.h @@ -115,9 +115,9 @@ class PipewireEnumerator : public SoundDeviceEnumerator { void updateAudioLatencyUsage(const SINT framesPerBuffer); void setLatency(unsigned int sampleRate, unsigned int framesPerBuffer); - void createInputPorts(const AudioInput& path); - void createOutputPorts(const AudioOutput& path); - uint32_t* createPorts(const AudioPath& path, bool channel); + std::pair createInputPorts(const AudioInput& path); + std::pair createOutputPorts(const AudioOutput& path); + std::pair createPorts(std::string_view name, spa_direction direction); struct Link { uint32_t input; From 033b92a46016dc2f17feb8b4e228f871e6a775aa Mon Sep 17 00:00:00 2001 From: Priyanshu Date: Wed, 8 Jul 2026 12:53:02 +0530 Subject: [PATCH 13/13] Set AudioInputBuffers to zero in case of null PipeWire buffer --- src/soundio/pipewireenumerator.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/soundio/pipewireenumerator.cpp b/src/soundio/pipewireenumerator.cpp index 8e91a685da55..65d99fa6b8cc 100644 --- a/src/soundio/pipewireenumerator.cpp +++ b/src/soundio/pipewireenumerator.cpp @@ -18,6 +18,7 @@ #include "soundio/soundmanager.h" #include "soundio/soundmanagerutil.h" #include "util/assert.h" +#include "util/sample.h" #include "util/trace.h" #include "util/types.h" #include "waveform/visualplayposition.h" @@ -530,6 +531,8 @@ void PipewireEnumerator::callback(const spa_io_position* pos) { pInputBuffer[i * 2] = buffer[i]; pInputBuffer[i * 2 + 1] = buffer[i]; } + } else { + SampleUtil::fill(pInputBuffer, 0, framesPerBuffer * 2); } } else { const float* bufferFL = static_cast( @@ -538,6 +541,10 @@ void PipewireEnumerator::callback(const spa_io_position* pos) { for (uint64_t i = 0; i < framesPerBuffer; i++) { pInputBuffer[iChannelBase + i * 2] = bufferFL[i]; } + } else { + for (uint64_t i = 0; i < framesPerBuffer; i++) { + pInputBuffer[iChannelBase + i * 2] = 0; + } } const float* bufferFR = static_cast( @@ -546,6 +553,10 @@ void PipewireEnumerator::callback(const spa_io_position* pos) { for (uint64_t i = 0; i < framesPerBuffer; i++) { pInputBuffer[iChannelBase + 1 + i * 2] = bufferFR[i]; } + } else { + for (uint64_t i = 0; i < framesPerBuffer; i++) { + pInputBuffer[iChannelBase + 1 + i * 2] = 0; + } } } }