Skip to content
Draft
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ if(ENABLE_JACK)
add_subdirectory(ui-jack)
endif()

if(NOT ENABLE_PULSEAUDIO AND NOT ENABLE_PIPEWIRE AND NOT ENABLE_JACK)
# Unified executable with runtime backend selection
if(ENABLE_PULSEAUDIO OR ENABLE_PIPEWIRE OR ENABLE_JACK)
add_subdirectory(ui-unified)
else()
message(FATAL_ERROR "At least one audio backend must be enabled (ENABLE_PULSEAUDIO, ENABLE_PIPEWIRE, or ENABLE_JACK).")
endif()
7 changes: 7 additions & 0 deletions src/common/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ add_library(projectM-Qt-Common STATIC
configfile.cpp
configfile.hpp
nullable.hpp
QAudioBackend.hpp
QAudioDeviceChooser.cpp
QAudioDeviceChooser.hpp
QAudioDeviceModel.cpp
QAudioDeviceModel.hpp
configutil.cpp
configutil.hpp
qplaylistfiledialog.cpp
qplaylistfiledialog.hpp
qplaylistmodel.cpp
Expand Down
51 changes: 51 additions & 0 deletions src/common/QAudioBackend.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#ifndef QAUDIO_BACKEND_HPP
#define QAUDIO_BACKEND_HPP

#include <QObject>
#include <QString>
#include <QList>

class QProjectM_MainWindow;
class QMutex;

class QAudioBackend : public QObject
{
Q_OBJECT

public:
struct DeviceInfo {
QString id;
QString displayName;
};

explicit QAudioBackend(QObject *parent = nullptr) : QObject(parent), m_active(false) {}
virtual ~QAudioBackend() {}

virtual bool start(QProjectM_MainWindow *mainWindow, QMutex *audioMutex) = 0;
virtual void stop() = 0;

bool isActive() const { return m_active; }
void setActive(bool active) { m_active = active; }

virtual QString backendName() const = 0;

virtual QList<DeviceInfo> devices() const = 0;
virtual QString currentDeviceId() const = 0;
virtual bool supportsDeviceSwitching() const = 0;

virtual void writeSettings() = 0;
virtual void readSettings() = 0;

protected:
bool m_active;

public slots:
virtual void selectDevice(const QString &deviceId) = 0;

signals:
void devicesChanged();
void activeDeviceChanged();
void errorOccurred(const QString &message);
};

#endif // QAUDIO_BACKEND_HPP
148 changes: 148 additions & 0 deletions src/common/QAudioDeviceChooser.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* projectM -- Milkdrop-esque visualisation SDK
* Copyright (C)2003-2004 projectM Team
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
* See 'LICENSE.txt' included within this release
*
*/

#include "QAudioDeviceChooser.hpp"
#include "QAudioDeviceModel.hpp"
#include "QAudioBackend.hpp"

#include <QCheckBox>
#include <QDialogButtonBox>
#include <QLabel>
#include <QListView>
#include <QSettings>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QtDebug>

QAudioDeviceChooser::QAudioDeviceChooser(QAudioBackend *backend, QWidget *parent)
: QDialog(parent),
m_backend(backend),
m_deviceModel(new QAudioDeviceModel(backend, this)),
m_devicesListView(nullptr),
m_autoDetectCheckBox(nullptr),
m_infoLabel(nullptr)
{
buildUi();
readSettings();
}

void QAudioDeviceChooser::buildUi()
{
setWindowTitle(tr("Audio Device Settings"));
resize(380, 271);

QVBoxLayout *mainVBox = new QVBoxLayout();

// Info / instruction label
m_infoLabel = new QLabel(tr("Select a source device below."), this);
mainVBox->addWidget(m_infoLabel);

// Device list view
m_devicesListView = new QListView(this);
m_devicesListView->setAutoFillBackground(true);
m_devicesListView->setToolTip(tr("Double click a source device to activate it."));
m_devicesListView->setModel(m_deviceModel);
mainVBox->addWidget(m_devicesListView);

// Auto-detect checkbox
m_autoDetectCheckBox = new QCheckBox(tr("Auto-detect best source"), this);
m_autoDetectCheckBox->setToolTip(
tr("Automatically select the best available audio source on startup. "
"This is the recommended way to get projectM to visualize sound "
"without specifying a device explicitly."));
mainVBox->addWidget(m_autoDetectCheckBox);

// If the backend does not support device switching, disable the list
if (!m_backend->supportsDeviceSwitching()) {
m_devicesListView->setEnabled(false);
m_autoDetectCheckBox->setEnabled(false);
m_infoLabel->setText(
tr("This audio backend does not support device switching."));
}

// Horizontal layout: main content + button box
QHBoxLayout *hBox = new QHBoxLayout(this);
hBox->addLayout(mainVBox);

QDialogButtonBox *buttonBox = new QDialogButtonBox(
QDialogButtonBox::Ok, Qt::Vertical, this);
hBox->addWidget(buttonBox);

setLayout(hBox);

// Connections
connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));

connect(m_autoDetectCheckBox, SIGNAL(stateChanged(int)),
this, SLOT(onAutoDetectToggled(int)));

connect(m_devicesListView, SIGNAL(doubleClicked(const QModelIndex&)),
this, SLOT(onDeviceDoubleClicked(const QModelIndex&)));
}

void QAudioDeviceChooser::onAutoDetectToggled(int state)
{
m_devicesListView->setEnabled(state != Qt::Checked);
}

void QAudioDeviceChooser::onDeviceDoubleClicked(const QModelIndex &index)
{
if (!index.isValid())
return;

QString deviceId = index.data(QAudioDeviceModel::DeviceIdRole).toString();
if (!deviceId.isEmpty()) {
m_backend->selectDevice(deviceId);
}
}

void QAudioDeviceChooser::writeSettings()
{
QSettings settings("projectM", "qprojectM");
settings.setValue("audioDeviceChooser/autoDetect",
m_autoDetectCheckBox->checkState() == Qt::Checked);

QString currentId = m_backend->currentDeviceId();
if (!currentId.isEmpty()) {
settings.setValue("audioDeviceChooser/deviceId", currentId);
}
}

void QAudioDeviceChooser::readSettings()
{
QSettings settings("projectM", "qprojectM");

bool autoDetect = settings.value("audioDeviceChooser/autoDetect", true).toBool();
m_autoDetectCheckBox->setCheckState(autoDetect ? Qt::Checked : Qt::Unchecked);

if (autoDetect) {
m_devicesListView->setEnabled(false);
} else {
m_devicesListView->setEnabled(true);
}
}

void QAudioDeviceChooser::open()
{
m_deviceModel->refresh();
show();
}
60 changes: 60 additions & 0 deletions src/common/QAudioDeviceChooser.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* projectM -- Milkdrop-esque visualisation SDK
* Copyright (C)2003-2004 projectM Team
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
* See 'LICENSE.txt' included within this release
*
*/

#ifndef QAUDIO_DEVICE_CHOOSER_HPP
#define QAUDIO_DEVICE_CHOOSER_HPP

#include <QDialog>

class QAudioBackend;
class QAudioDeviceModel;
class QListView;
class QCheckBox;
class QLabel;

class QAudioDeviceChooser : public QDialog
{
Q_OBJECT

public:
QAudioDeviceChooser(QAudioBackend *backend, QWidget *parent = nullptr);

public slots:
void open();
void writeSettings();

private slots:
void readSettings();
void onAutoDetectToggled(int state);
void onDeviceDoubleClicked(const QModelIndex &index);

private:
void buildUi();

QAudioBackend *m_backend;
QAudioDeviceModel *m_deviceModel;

QListView *m_devicesListView;
QCheckBox *m_autoDetectCheckBox;
QLabel *m_infoLabel;
};

#endif // QAUDIO_DEVICE_CHOOSER_HPP
83 changes: 83 additions & 0 deletions src/common/QAudioDeviceModel.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* projectM -- Milkdrop-esque visualisation SDK
* Copyright (C)2003-2004 projectM Team
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
* See 'LICENSE.txt' included within this release
*
*/

#include "QAudioDeviceModel.hpp"
#include "QAudioBackend.hpp"

#include <QColor>

QAudioDeviceModel::QAudioDeviceModel(QAudioBackend *backend, QObject *parent)
: QAbstractListModel(parent), m_backend(backend)
{
connect(m_backend, SIGNAL(devicesChanged()), this, SLOT(refresh()));
connect(m_backend, SIGNAL(activeDeviceChanged()), this, SLOT(refresh()));
}

QAudioDeviceModel::~QAudioDeviceModel()
{
}

void QAudioDeviceModel::refresh()
{
beginResetModel();
endResetModel();
}

int QAudioDeviceModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_backend->devices().size();
}

QVariant QAudioDeviceModel::data(const QModelIndex &index, int role) const
{
QList<QAudioBackend::DeviceInfo> devs = m_backend->devices();
Comment on lines +26 to +52

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

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

QAudioDeviceModel::data() rebuilds a fresh devices() list on every call (and rowCount() also calls devices()). This can be expensive and can lead to inconsistent row->device mapping if the backend returns devices in non-stable order. Consider caching the device list in the model (update it in refresh()), and have rowCount()/data() read from that cached list.

Suggested change
QAudioDeviceModel::QAudioDeviceModel(QAudioBackend *backend, QObject *parent)
: QAbstractListModel(parent), m_backend(backend)
{
connect(m_backend, SIGNAL(devicesChanged()), this, SLOT(refresh()));
connect(m_backend, SIGNAL(activeDeviceChanged()), this, SLOT(refresh()));
}
QAudioDeviceModel::~QAudioDeviceModel()
{
}
void QAudioDeviceModel::refresh()
{
beginResetModel();
endResetModel();
}
int QAudioDeviceModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_backend->devices().size();
}
QVariant QAudioDeviceModel::data(const QModelIndex &index, int role) const
{
QList<QAudioBackend::DeviceInfo> devs = m_backend->devices();
#include <QHash>
namespace
{
QHash<const QAudioDeviceModel *, QList<QAudioBackend::DeviceInfo> > s_deviceCache;
}
QAudioDeviceModel::QAudioDeviceModel(QAudioBackend *backend, QObject *parent)
: QAbstractListModel(parent), m_backend(backend)
{
s_deviceCache.insert(this, m_backend->devices());
connect(m_backend, SIGNAL(devicesChanged()), this, SLOT(refresh()));
connect(m_backend, SIGNAL(activeDeviceChanged()), this, SLOT(refresh()));
}
QAudioDeviceModel::~QAudioDeviceModel()
{
s_deviceCache.remove(this);
}
void QAudioDeviceModel::refresh()
{
beginResetModel();
s_deviceCache[this] = m_backend->devices();
endResetModel();
}
int QAudioDeviceModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return s_deviceCache.value(this).size();
}
QVariant QAudioDeviceModel::data(const QModelIndex &index, int role) const
{
const QList<QAudioBackend::DeviceInfo> devs = s_deviceCache.value(this);

Copilot uses AI. Check for mistakes.

if (!index.isValid() || index.row() >= devs.size())
return QVariant();

const QAudioBackend::DeviceInfo &info = devs[index.row()];

switch (role)
{
case Qt::DisplayRole:
return info.displayName;

case Qt::ToolTipRole:
if (info.id == m_backend->currentDeviceId())
return info.displayName + " (active)";
else
return info.displayName;

case Qt::BackgroundRole:
if (info.id == m_backend->currentDeviceId()) {
QColor highlight(0, 200, 0, 80);
return highlight;
}
return QVariant();

case DeviceIdRole:
return info.id;

default:
return QVariant();
}
}
Loading