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/preferences/dialog/dlgprefsound.cpp b/src/preferences/dialog/dlgprefsound.cpp index 316a1ebae989..c7130c771a9c 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); @@ -108,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, @@ -508,14 +518,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 +816,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. @@ -1099,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 86a05ae32e15..490e9a9fd0ee 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,10 @@ 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); + void updateSampleRates(const QList& sampleRates); 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/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..65d99fa6b8cc --- /dev/null +++ b/src/soundio/pipewireenumerator.cpp @@ -0,0 +1,779 @@ +#include "soundio/pipewireenumerator.h" + +#include +#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 { + +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) { + 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_pPwThreadLoop(nullptr), + m_pPwContext(nullptr), + m_pPwCore(nullptr), + m_pPwRegistry(nullptr), + m_pPwMetadata(nullptr), + m_pPwFilter(nullptr), + m_initialized(false), + 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); + + pw_init(nullptr, nullptr); + + m_pPwThreadLoop = pw_thread_loop_new("mixxx_loop", nullptr); + spa_zero(m_pwRegistryListener); + spa_zero(m_pwMetadataListener); + spa_zero(m_pwFilterListener); + + initialize(); +} + +PipewireEnumerator::~PipewireEnumerator() { + pw_thread_loop_stop(m_pPwThreadLoop); + + if (m_pPwFilter) { + spa_hook_remove(&m_pwFilterListener); + 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() { + if (m_initialized) { + return; + } + + 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_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_pPwFilter = pw_filter_new(m_pPwCore, + "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)); + + pw_filter_add_listener(m_pPwFilter, &m_pwFilterListener, &filter_events, this); + + for (auto it = m_inputs.begin(); it != m_inputs.end(); ++it) { + it.value() = createInputPorts(it.key()); + } + + for (auto it = m_outputs.begin(); it != m_outputs.end(); ++it) { + it.value() = createOutputPorts(it.key()); + } + + int res = pw_filter_connect(m_pPwFilter, + PW_FILTER_FLAG_RT_PROCESS, + nullptr, + 0); + + VERIFY_OR_DEBUG_ASSERT(res >= 0) { + qWarning() << "PipewireEnumerator::initialize pw_filter_connect error:" + << spa_strerror(res); + } + + pw_thread_loop_start(m_pPwThreadLoop); + + m_initialized = true; +} + +QList PipewireEnumerator::getSampleRates() const { + 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_pPwRegistry, + id, + PW_TYPE_INTERFACE_Metadata, + PW_VERSION_METADATA, + 0); + 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); + + 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); + emit deviceAdded(pDevice); + // pipewire assigns each object with a unique ID + // any previous element is either invalid or already removed + 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)); + if (!m_soundDevices.contains(node_id)) { + // most likely midi or video node + return; + } + + m_objects.insert_or_assign(id, Object{Port(node_id)}); + QSharedPointer pSoundDevice = m_soundDevices.at(node_id); + pSoundDevice->registerPort(id, pProps); + m_pSoundManager->updateDeviceChannels(pSoundDevice); + + const char* direction = spa_dict_lookup(pProps, PW_KEY_PORT_DIRECTION); + + 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; + } + + if (list.at(1) == "FL") { + *m_inputs.value(*it).first = id; + } else { + *m_inputs.value(*it).second = id; + } + } else { + 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; + } + } + } + } 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) { + 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); + } + } +} + +void PipewireEnumerator::registryEventGlobalRemove(unsigned int id) { + if (!m_objects.contains(id)) { + return; + } + + auto pair = m_objects.extract(id); + Object& object = pair.mapped(); + + if (std::get_if(&object)) { + if (!m_soundDevices.contains(id)) { + return; + } + + QSharedPointer pDevice = m_soundDevices.at(id); + if (pDevice->isOpen()) { + pDevice->close(); + } + + m_soundDevices.erase(id); + emit deviceRemoved(pDevice); + // m_pSoundManager->removeDevice(device); + } else if (Port* port = std::get_if(&object)) { + VERIFY_OR_DEBUG_ASSERT(m_soundDevices.contains(port->node)) { + return; + } + + 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; + for (const auto& [id, pDevice] : m_soundDevices) { + devices.push_back(pDevice); + } + + return devices; +} + +int PipewireEnumerator::metadataProperty( + void* data, uint32_t, const char* key, const char*, const char* value) { + PipewireEnumerator* pEnumerator = static_cast(data); + + 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('['); + 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())); + } + } + return 0; +} + +bool PipewireEnumerator::isOpen(uint32_t id) { + return std::ranges::find(m_openedDevices, id) != m_openedDevices.end(); +} + +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 "PipewireEnumerator uninitialized"; + } + + if (sampleRate != m_sampleRate || framesPerBuffer != m_framesPerBuffer) { + setLatency(sampleRate, framesPerBuffer); + } + + 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); + + // 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(); + }); + + 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(); + + if (channelCount == 1) { + uint32_t filterPort = channelBase % 2 ? *filterPorts.second : *filterPorts.first; + result += createLink(deviceId, ports[channelBase].id, m_filterId, filterPort); + } else { + result += createLink(deviceId, ports[channelBase].id, m_filterId, *filterPorts.first); + result += createLink(deviceId, + ports[channelBase + 1].id, + m_filterId, + *filterPorts.second); + } + } + + // 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); + m_openedDevices.push_back(deviceId); + return result; +} + +void PipewireEnumerator::closeDevice(uint32_t id) { + VERIFY_OR_DEBUG_ASSERT(m_initialized) { + qWarning() << "PipewireEnumerator::closeDevice called when " + "uninitialized, this should not happen"; + return; + } + + auto deviceId = std::ranges::find(m_openedDevices, id); + + VERIFY_OR_DEBUG_ASSERT(deviceId != m_openedDevices.end()) { + qWarning() << "device:" << id << "not opened"; + return; + } + + QSharedPointer pDevice = m_soundDevices.at(*deviceId); + + // device m_inLinks and m_outLinks are cleared by link registryEventGlobalRemove + for (uint32_t link : pDevice->getInLinks()) { + destroyLink(link); + } + + for (uint32_t link : pDevice->getOutLinks()) { + destroyLink(link); + } + + m_openedDevices.erase(deviceId); +} + +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 uint32_t sampleRate = pos->clock.rate.denom; + const uint64_t framesPerBuffer = pos->clock.duration; + + if (sampleRate != m_sampleRate || framesPerBuffer != m_framesPerBuffer) { + qWarning() << "PipewireEnumerator::callback" + "requested" + << m_framesPerBuffer << "samples at" << m_sampleRate << "hz," + "provided" + << framesPerBuffer << "samples at" << sampleRate << "hz"; + setLatency(sampleRate, framesPerBuffer); + } + + qDebug() << "PipewireEnumerator::callback" << sampleRate << framesPerBuffer; + m_pSoundManager->processUnderflowHappened(framesPerBuffer); + + for (uint32_t deviceId : m_openedDevices) { + QSharedPointer device = m_soundDevices.at(deviceId); + QList deviceInputs = device->inputs(); + for (const 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); + + 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 { + SampleUtil::fill(pInputBuffer, 0, framesPerBuffer * 2); + } + } else { + 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]; + } + } else { + for (uint64_t i = 0; i < framesPerBuffer; i++) { + pInputBuffer[iChannelBase + i * 2] = 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]; + } + } else { + for (uint64_t i = 0; i < framesPerBuffer; i++) { + pInputBuffer[iChannelBase + 1 + i * 2] = 0; + } + } + } + } + m_pSoundManager->pushInputBuffers(deviceInputs, framesPerBuffer); + } + + m_pSoundManager->onDeviceOutputCallback(framesPerBuffer); + + for (uint32_t deviceId : m_openedDevices) { + QSharedPointer device = m_soundDevices.at(deviceId); + for (const 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); + + 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 * 2]; + } + } + } + } + } + + 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::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); + + 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* pProxy = static_cast(pw_core_create_object(m_pPwCore, + "link-factory", + PW_TYPE_INTERFACE_Link, + PW_VERSION_LINK, + &props, + 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); +} + +void PipewireEnumerator::registerInput(const AudioInput& input, AudioDestination*) { + if (m_inputs.contains(input) or input.isHidden()) { + // duplicate VinylControl signal + return; + } + + if (m_initialized) { + pw_thread_loop_lock(m_pPwThreadLoop); + 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); + 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 +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, "%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); + + 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 +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 +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) { + 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 new file mode 100644 index 000000000000..c8478bab164b --- /dev/null +++ b/src/soundio/pipewireenumerator.h @@ -0,0 +1,169 @@ +#pragma once + +#include +#include +#include + +#include + +#include "audio/types.h" +#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 m_initialized ? std::vector{"PipeWire"} : std::vector{}; + } + + void initialize(); + + bool isOpen(uint32_t id); + std::string openDevice(const SoundDevicePipewire& device, + mixxx::audio::SampleRate sampleRate, + SINT framesPerBuffer); + void closeDevice(uint32_t id); + 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, + 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); + + 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); + 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; + uint32_t output; + }; + + struct Port { + uint32_t node; + }; + + struct Node {}; + using Object = std::variant; + + std::unordered_map m_objects; + QList m_samplerates; + + SoundManager* m_pSoundManager; + UserSettingsPointer m_pConfig; + + 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; + + std::unordered_map> m_soundDevices; + std::vector m_openedDevices; + + bool m_initialized; + uint64_t xrun_duration; + int m_invalidTimeInfoCount; + double m_lastCallbackEntrytoDacSecs; + PerformanceTimer m_clkRefTimer; + mixxx::audio::SampleRate m_sampleRate; + mixxx::audio::SampleRate m_defaultSampleRate; + + 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/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 new file mode 100644 index 000000000000..c6561bbded17 --- /dev/null +++ b/src/soundio/sounddevicepipewire.cpp @@ -0,0 +1,182 @@ +#include "sounddevicepipewire.h" + +#include + +#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, + SoundManager* pManager, + PipewireEnumerator* pEnumerator, + uint32_t id, + const std::string_view name) + : SoundDevice(pConfig, pManager), + m_pEnumerator(pEnumerator) { + m_hostAPI = QStringLiteral("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); + m_sampleRate = getDefaultSampleRate(); +} + +SoundDeviceStatus SoundDevicePipewire::open(bool, int) { + m_error = m_pEnumerator->openDevice(*this, m_sampleRate, m_configFramesPerBuffer); + if (m_error.empty()) { + return SoundDeviceStatus::Ok; + } else { + return SoundDeviceStatus::Error; + } +} + +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::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 (!nameStr) { + nameStr = spa_dict_lookup(props, PW_KEY_PORT_NAME); + } + + if (nameStr) { + name = nameStr; + } else { + name = direction; + name += ":"; + name += spa_dict_lookup(props, PW_KEY_PORT_ID); + } + + // 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(direction, "out") == 0) { + m_outPorts.emplace_back(id, name); + m_numInputChannels = mixxx::audio::ChannelCount::fromInt(m_outPorts.size()); + } +} + +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); + return; + } + } + for (auto it = m_outPorts.begin(); it != m_outPorts.end(); it++) { + if (it->id == id) { + m_outPorts.erase(it); + return; + } + } +} + +mixxx::audio::SampleRate SoundDevicePipewire::getDefaultSampleRate() const { + auto defaultSampleRate = m_pEnumerator->getDefaultSampleRate(); + if (defaultSampleRate.isValid()) { + return defaultSampleRate; + } + + 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 new file mode 100644 index 000000000000..0d6cb30acb5e --- /dev/null +++ b/src/soundio/sounddevicepipewire.h @@ -0,0 +1,73 @@ +#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 m_error.c_str(); + } + + 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); + + void createLink(uint32_t outNodeId, + uint32_t outPortId, + uint32_t inNodeId, + uint32_t inPortId); + 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; + 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; + + std::vector m_inLinks; + std::vector m_outLinks; + std::string m_error; +}; 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/soundmanager.cpp b/src/soundio/soundmanager.cpp index 4909393bb7b8..6b1872e43cd5 100644 --- a/src/soundio/soundmanager.cpp +++ b/src/soundio/soundmanager.cpp @@ -14,6 +14,7 @@ #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 +22,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 +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(pConfig, this), - m_networkEnumerator(pConfig, this) { + m_pPaEnumerator(std::make_unique(pConfig, this)), +#ifdef __PIPEWIRE__ + m_pPipewireEnumerator(std::make_unique(pConfig, this)), +#endif + 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( @@ -95,7 +103,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 +111,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 +120,7 @@ QList SoundManager::getDeviceList( (!hasInputs && !hasOutputs)) { continue; } + filteredDeviceList.push_back(pDevice); } @@ -120,9 +130,15 @@ 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_pPipewireEnumerator->getAPIs()) { apiList.push_back(api.c_str()); } +#endif return apiList; } @@ -215,17 +231,29 @@ void SoundManager::clearDeviceList(bool sleepAfterClosing) { m_devices.clear(); m_pErrorDevice.clear(); - m_paEnumerator.terminate(); + m_pPaEnumerator->terminate(); } 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(); - } else if (!api.isEmpty()) { - return m_paEnumerator.getSampleRates(); + samplerates = m_pPaEnumerator->getJackSampleRates(); + } +#ifdef __PIPEWIRE__ + else if (api == MIXXX_PIPEWIRE_STRING) { + samplerates = m_pPipewireEnumerator->getSampleRates(); + } +#endif + else if (!api.isEmpty()) { + samplerates = m_pPaEnumerator->getSampleRates(); } + + if (!samplerates.empty()) { + return samplerates; + } + return QList{ mixxx::audio::SampleRate(44100), mixxx::audio::SampleRate(48000), @@ -234,20 +262,30 @@ QList SoundManager::getSampleRates(const QString& api) } QList SoundManager::getSampleRates() const { - return getSampleRates(""); + return getSampleRates(m_config.getAPI()); } void SoundManager::queryDevices() { qDebug() << "SoundManager::queryDevices()"; - m_paEnumerator.initialize(); + m_devices.clear(); + m_pPaEnumerator->initialize(); + + for (auto& device : m_pPaEnumerator->queryDevices()) { + m_devices.push_back(device); + qDebug() << "m_devices.push_back " << device->getDisplayName(); + } - for (auto& device : m_paEnumerator.queryDevices()) { - m_devices.push_back(SoundDevicePointer(device)); +#ifdef __PIPEWIRE__ + 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()) { - m_devices.push_back(SoundDevicePointer(device)); + for (auto& device : m_pNetworkEnumerator->queryDevices()) { + m_devices.push_back(device); + qDebug() << "m_devices.push_back " << device->getDisplayName(); } // now tell the prefs that we updated the device list -- bkgood @@ -304,6 +342,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(); @@ -623,3 +662,24 @@ 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); +} + +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; + } + } +} + +void SoundManager::updateDeviceChannels(SoundDevicePointer pDevice) { + emit deviceChannelsUpdated(pDevice); +} diff --git a/src/soundio/soundmanager.h b/src/soundio/soundmanager.h index 1138e7cffa08..980f8022c9ab 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_pNetworkEnumerator->getNetworkStream(); } void underflowHappened(int code) { @@ -109,7 +111,16 @@ 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 @@ -119,6 +130,10 @@ class SoundManager : public QObject { 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); @@ -150,6 +165,11 @@ class SoundManager : public QObject { PollingControlProxy m_audioLatencyOverloadCount; PollingControlProxy m_audioLatencyOverload; - PortAudioEnumerator m_paEnumerator; - NetworkEnumerator m_networkEnumerator; + std::unique_ptr m_pPaEnumerator; + +#ifdef __PIPEWIRE__ + std::unique_ptr m_pPipewireEnumerator; +#endif + + std::unique_ptr m_pNetworkEnumerator; }; 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) { 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 \