From a97bfbe96bcb33d53da2db4c94af2d102e43a681 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 15 May 2025 23:28:05 -0400 Subject: [PATCH 01/19] Full Qt6 support --- CMakeLists.txt | 1 + README.md | 11 +++- mainwindow.cpp | 175 +++++++++++++++++++++++++++++-------------------- mainwindow.h | 18 ++--- mainwindow.ui | 25 +++++-- reader.cpp | 2 +- reader.h | 6 +- 7 files changed, 146 insertions(+), 92 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 54207e6..1cd903c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,6 +85,7 @@ foreach(_lib ${THIRD_PARTY_DLLS}) endforeach() if(Qt6_FOUND) + set(MACOSX_BUNDLE_GUI_IDENTIFIER "com.labstreaminglayer.AudioCapture") qt_finalize_executable(${PROJECT_NAME}) endif() diff --git a/README.md b/README.md index 8490b32..4f59a5c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Overview -The AudioCapture application uses Qt's [QAudioInput](https://doc.qt.io/qt-5/qaudioinput.html) for cross-platform audio capturing. This program has been tested on Windows and MacOS. Download the latest version [from the releases page](https://github.com/labstreaminglayer/App-AudioCapture/releases). +The AudioCapture application uses Qt's [QAudioInput](https://doc.qt.io/qt-5/qaudioinput.html) for cross-platform audio capturing. +This program has been tested on Windows and MacOS. Download the latest version [from the releases page](https://github.com/labstreaminglayer/App-AudioCapture/releases). The Windows release requires vc_redist.x64.exe [from Microsoft](https://support.microsoft.com/en-gb/help/2977003/the-latest-supported-visual-c-downloads). @@ -32,4 +33,10 @@ Note that code-signing has been disabled. The previous version of AudioCaptureWin can be found as [release v0.1](https://github.com/labstreaminglayer/App-AudioCapture/releases/tag/v0.1) in this repository. -For Windows XP there is an older LSL audio recording app available on request; it uses the [irrKlang](http://www.ambiera.com/irrklang/) audio library, which in turn uses DirectX audio on Windows. That application does not support support accurate time synchronization and is therefore deprecated. +For Windows XP there is an older LSL audio recording app available on request; it uses the [irrKlang](http://www.ambiera.com/irrklang/) audio library, which in turn uses DirectX audio on Windows. That application does not support accurate time synchronization and is therefore deprecated. + +# Developer Notes + +There were quite a few changes with Qt6. We follow the general pattern outlined [here](https://doc.qt.io/qt-6/audiooverview.html#low-level-audio-playback-and-recording). + +> Conversely, for pull mode with QAudioSource, when audio data is available then the data will be written directly to the QIODevice. diff --git a/mainwindow.cpp b/mainwindow.cpp index e9fe2bb..a9a7697 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -2,7 +2,7 @@ #include "reader.h" #include "ui_mainwindow.h" -#include +#include #include #include #include @@ -16,44 +16,73 @@ #include #include -lsl::channel_format_t bits2fmt(int bits) { - if (bits == 8) return lsl::cf_int8; - if (bits == 16) return lsl::cf_int16; - if (bits == 32) return lsl::cf_float32; - // if (bits == 64) return lsl::cf_double64; - throw std::runtime_error("Unsupported sample bits."); +lsl::channel_format_t sampleFormatToLSL(QAudioFormat::SampleFormat fmt) { + switch (fmt) { + case QAudioFormat::Float: return lsl::cf_float32; + case QAudioFormat::Int32: return lsl::cf_int32; + case QAudioFormat::Int16: return lsl::cf_int16; + case QAudioFormat::UInt8: return lsl::cf_int8; + default: return lsl::cf_float32; + } +} + +// Helper to convert SampleFormat to string +QString sampleFormatToString(QAudioFormat::SampleFormat fmt) { + switch (fmt) { + case QAudioFormat::UInt8: return "UInt8"; + case QAudioFormat::Int16: return "Int16"; + case QAudioFormat::Int32: return "Int32"; + case QAudioFormat::Float: return "Float"; + default: return "Unknown"; + } +} + +// Helper to convert string to SampleFormat +QAudioFormat::SampleFormat stringToSampleFormat(const QString &str) { + if (str == "UInt8") return QAudioFormat::UInt8; + if (str == "Int16") return QAudioFormat::Int16; + if (str == "Int32") return QAudioFormat::Int32; + if (str == "Float") return QAudioFormat::Float; + return QAudioFormat::Unknown; } MainWindow::MainWindow(QWidget *parent, const char *config_file) : QMainWindow(parent), ui(new Ui::MainWindow), - devices(QAudioDeviceInfo::availableDevices(QAudio::Mode::AudioInput)) { + devices(QMediaDevices::audioInputs()) { if(devices.empty()) { QMessageBox::warning(this, "Fatal error", "No capture devices found, quitting."); exit(1); } ui->setupUi(this); + connect(ui->actionLoad_Configuration, &QAction::triggered, [this]() { load_config(QFileDialog::getOpenFileName( this, "Load Configuration File", "", "Configuration Files (*.cfg)")); }); + connect(ui->actionSave_Configuration, &QAction::triggered, [this]() { save_config(QFileDialog::getSaveFileName( this, "Save Configuration File", "", "Configuration Files (*.cfg)")); }); + connect(ui->actionQuit, &QAction::triggered, this, &MainWindow::close); + connect(ui->actionAbout, &QAction::triggered, [this]() { QString infostr = QStringLiteral("LSL library version: ") + QString::number(lsl::library_version()) + "\nLSL library info:" + lsl::library_info(); QMessageBox::about(this, "About this app", infostr); }); + connect(ui->linkButton, &QPushButton::clicked, this, &MainWindow::toggleRecording); // audio devices - for (auto info : devices) ui->input_device->addItem(info.deviceName()); + for (const auto &dev : devices) ui->input_device->addItem(dev.description()); auto changeSignal = static_cast(&QComboBox::currentIndexChanged); + connect(ui->input_device, changeSignal, this, &MainWindow::deviceChanged); deviceChanged(); + connect(ui->btn_checkfmt, &QPushButton::clicked, this, &MainWindow::checkAudioFormat); QString cfgfilepath = find_config_file(config_file); @@ -61,83 +90,85 @@ MainWindow::MainWindow(QWidget *parent, const char *config_file) checkAudioFormat(); } -QAudioDeviceInfo MainWindow::currentDeviceInfo() { +QAudioDevice MainWindow::currentDevice() const { return devices.at(ui->input_device->currentIndex()); } void MainWindow::deviceChanged() { - auto info = currentDeviceInfo(); - updateComboBoxItems(ui->input_channels, info.supportedChannelCounts()); - updateComboBoxItems(ui->input_samplerate, info.supportedSampleRates()); - updateComboBoxItems(ui->input_samplesize, info.supportedSampleSizes()); - QAudioFormat fmt(info.preferredFormat()); - if ((fmt.sampleSize() == 8 || fmt.sampleSize() == 24) && info.supportedSampleSizes().contains(16)) fmt.setSampleSize(16); + auto const dev = currentDevice(); + + // Channel count + ui->input_channels->setMinimum(dev.minimumChannelCount()); + ui->input_channels->setMaximum(dev.maximumChannelCount()); + ui->input_channels->setValue(dev.preferredFormat().channelCount()); + // Sample rate + ui->input_samplerate->setMinimum(dev.minimumSampleRate()); + ui->input_samplerate->setMaximum(dev.maximumSampleRate()); + ui->input_samplerate->setValue(dev.preferredFormat().sampleRate()); + // Sample format + ui->input_samplesize->clear(); + for (auto samp_fmt : dev.supportedSampleFormats()) { + ui->input_samplesize->addItem(sampleFormatToString(samp_fmt), QVariant::fromValue(samp_fmt)); + } + auto const fmt(dev.preferredFormat()); + if (const auto idx = ui->input_samplesize->findData(QVariant::fromValue(fmt.sampleFormat())); idx >= 0) ui->input_samplesize->setCurrentIndex(idx); setFmt(fmt); } -QAudioFormat MainWindow::selectedAudioFormat() { - auto info = currentDeviceInfo(); - QAudioFormat fmt(info.preferredFormat()); - fmt.setByteOrder(QAudioFormat::LittleEndian); - fmt.setSampleType(QAudioFormat::SampleType::SignedInt); +QAudioFormat MainWindow::selectedAudioFormat() const { + const auto dev = currentDevice(); + QAudioFormat fmt(dev.preferredFormat()); qInfo() << "Preferred: " << fmt; - fmt.setSampleRate(ui->input_samplerate->currentText().toInt()); - fmt.setSampleSize(ui->input_samplesize->currentText().toInt()); - fmt.setChannelCount(ui->input_channels->currentText().toInt()); + fmt.setSampleRate(ui->input_samplerate->value()); + fmt.setChannelCount(ui->input_channels->value()); + // fmt.setByteOrder(QAudioFormat::LittleEndian); + // fmt.setChannelConfig(??); + fmt.setSampleFormat(stringToSampleFormat(ui->input_samplesize->currentText())); return fmt; } void MainWindow::setFmt(const QAudioFormat &fmt) { qInfo() << "Setting fmt: " << fmt; - ui->input_samplerate->setCurrentText(QString::number(fmt.sampleRate())); - ui->input_samplesize->setCurrentText(QString::number(fmt.sampleSize())); - ui->input_channels->setCurrentText(QString::number(fmt.channelCount())); + ui->input_samplerate->setValue(fmt.sampleRate()); + ui->input_samplesize->setCurrentText(sampleFormatToString(fmt.sampleFormat())); + ui->input_channels->setValue(fmt.channelCount()); auto fmtStr = QStringLiteral("%1 channels, %2 bit @ %3 Hz") .arg(fmt.channelCount()) - .arg(fmt.sampleSize()) + .arg(fmt.sampleFormat()) .arg(fmt.sampleRate()); ui->label_fmtresult->setText(fmtStr); } void MainWindow::checkAudioFormat() { auto fmt = selectedAudioFormat(); - auto info = currentDeviceInfo(); - if (info.isFormatSupported(fmt)) + if (const auto dev = currentDevice(); dev.isFormatSupported(fmt)) qInfo() << "Format is supported"; else { QMessageBox::warning(this, "Format not supported", - "The requested format isn't supported; a supported format was automatically selected."); - fmt = info.nearestFormat(fmt); + "The requested format isn't supported; the preferred format was automatically selected."); + fmt = dev.preferredFormat(); } setFmt(fmt); } -void MainWindow::updateComboBoxItems(QComboBox *box, QList values) { - const int lastValue = box->currentText().toInt(); - box->clear(); - for (int value : values) { - box->addItem(QString::number(value)); - if (lastValue == value) box->setCurrentIndex(box->count() - 1); - } -} - -void MainWindow::load_config(const QString &filename) { - QSettings settings(filename, QSettings::Format::IniFormat); +void MainWindow::load_config(const QString &filename) const { + const QSettings settings(filename, QSettings::Format::IniFormat); ui->input_name->setText(settings.value("AudioCapture/name", "MyAudioStream").toString()); ui->input_device->setCurrentIndex(settings.value("AudioCapture/device", 0).toInt()); - ui->input_samplerate->setCurrentIndex(settings.value("AudioCapture/samplerate", 1).toInt()); - ui->input_samplesize->setCurrentIndex(settings.value("AudioCapture/samplesize", 1).toInt()); - ui->input_channels->setCurrentIndex(settings.value("AudioCapture/channels", 0).toInt()); + ui->input_samplerate->setValue(settings.value("AudioCapture/samplerate", 1).toInt()); + const QString sampleSize = settings.value("AudioCapture/samplesize", "Int16").toString(); + ui->input_samplesize->setCurrentIndex(ui->input_samplesize->findData(QVariant::fromValue(sampleSize))); + ui->input_channels->setValue(settings.value("AudioCapture/channels", 0).toInt()); } -void MainWindow::save_config(const QString &filename) { +void MainWindow::save_config(const QString &filename) const { QSettings settings(filename, QSettings::Format::IniFormat); settings.beginGroup("AudioCapture"); settings.setValue("name", ui->input_name->text()); settings.setValue("device", ui->input_device->currentIndex()); - settings.setValue("samplerate", ui->input_samplerate->currentIndex()); - settings.setValue("samplesize", ui->input_samplesize->currentIndex()); - settings.setValue("channels", ui->input_channels->currentIndex()); + settings.setValue("samplerate", ui->input_samplerate->value()); + settings.setValue("samplesize", ui->input_samplesize->currentText()); + settings.setValue("channels", ui->input_channels->value()); settings.sync(); } @@ -151,35 +182,39 @@ void MainWindow::closeEvent(QCloseEvent *ev) { void MainWindow::toggleRecording() { if (!reader) { // read the configuration from the UI fields - std::string name = ui->input_name->text().toStdString(); + auto const name = ui->input_name->text().toStdString(); auto fmt = selectedAudioFormat(); - int channel_count = fmt.channelCount(); - int samplerate = fmt.sampleRate(); - auto channel_format = bits2fmt(fmt.sampleSize()); - - std::string stream_id = currentDeviceInfo().deviceName().toStdString(); + auto const channel_count = fmt.channelCount(); + auto const samplerate = fmt.sampleRate(); + auto const channel_format = sampleFormatToLSL(fmt.sampleFormat()); + auto const stream_id = currentDevice().description().toStdString(); + // Create the LSL stream info lsl::stream_info info(name, "Audio", channel_count, samplerate, channel_format, stream_id); info.desc().append_child("provider").append_child_value("api", "QtMultimedia"); info.desc().append_child_value("device", ui->input_device->currentText().toStdString()); - audiodev = std::make_unique(currentDeviceInfo(), fmt, this); - auto buffer_ms = ui->input_buffersize->value(); - audiodev->setBufferSize(fmt.bytesForDuration(2 * buffer_ms * 1000)); + // Create and open the QIODevice that will receive the audio data reader = std::make_unique(lsl::stream_outlet(info)); reader->open(QIODevice::OpenModeFlag::WriteOnly); - audiodev->start(&*reader); - qInfo() << audiodev->state() << ' ' << audiodev->error(); + // Create the AudioSource + audiosrc = std::make_unique(currentDevice(), fmt, this); + // auto const buffer_ms = ui->input_buffersize->value(); + // audiosrc->setBufferSize(fmt.bytesForDuration(2 * buffer_ms * 1000)); + + // Start sinking audio data to the LSL IO device + audiosrc->start(&*reader); + qInfo() << audiosrc->state() << ' ' << audiosrc->error(); ui->linkButton->setText("Unlink"); } else { qInfo() << "Read " << reader->pos() << " bytes, " << reader->samples_written() << " samples, " << - ((double) reader->samples_written()/audiodev->format().sampleRate()) << 's'; - audiodev->stop(); - qInfo() << audiodev->state() << ' ' << audiodev->error(); + (static_cast(reader->samples_written())/audiosrc->format().sampleRate()) << 's'; + audiosrc->stop(); + qInfo() << audiosrc->state() << ' ' << audiosrc->error(); reader->close(); - audiodev = nullptr; + audiosrc = nullptr; reader = nullptr; ui->linkButton->setText("Link"); } @@ -189,7 +224,7 @@ void MainWindow::toggleRecording() { /** * Find a config file to load. This is (in descending order or preference): * - a file supplied on the command line - * - [executablename].cfg in one the the following folders: + * - [executablename].cfg in one the following folders: * - the current working directory * - the default config folder, e.g. '~/Library/Preferences' on OS X * - the executable folder @@ -206,16 +241,16 @@ QString MainWindow::find_config_file(const char *filename) { else return qfilename; } - QFileInfo exeInfo(QCoreApplication::applicationFilePath()); - QString defaultCfgFilename(exeInfo.completeBaseName() + ".cfg"); + const QFileInfo exeInfo(QCoreApplication::applicationFilePath()); + const QString defaultCfgFilename(exeInfo.completeBaseName() + ".cfg"); QStringList cfgpaths; cfgpaths << QDir::currentPath() << QStandardPaths::standardLocations(QStandardPaths::ConfigLocation) << exeInfo.path(); - for (auto path : cfgpaths) { + for (const auto& path : cfgpaths) { QString cfgfilepath = path + QDir::separator() + defaultCfgFilename; if (QFileInfo::exists(cfgfilepath)) return cfgfilepath; } - QMessageBox(QMessageBox::Warning, "No config file not found", + QMessageBox msg_box(QMessageBox::Warning, "No config file not found", QStringLiteral("No default config file could be found"), QMessageBox::Ok, this); return ""; } diff --git a/mainwindow.h b/mainwindow.h index 4602b5f..acb36d0 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -1,7 +1,8 @@ #ifndef MAINWINDOW_H #define MAINWINDOW_H #include "ui_mainwindow.h" -#include +#include +#include #include #include //for std::unique_ptr @@ -23,20 +24,19 @@ private slots: private: // Audio device handling - QAudioDeviceInfo currentDeviceInfo(); + QAudioDevice currentDevice() const; void setFmt(const QAudioFormat &fmt); - QAudioFormat selectedAudioFormat(); - void updateSampleRates(); - void updateComboBoxItems(QComboBox *box, QList values); + QAudioFormat selectedAudioFormat() const; + // void updateSampleRates(); // function for loading / saving the config file QString find_config_file(const char *filename); - void load_config(const QString &filename); - void save_config(const QString &filename); + void load_config(const QString &filename) const; + void save_config(const QString &filename) const; std::unique_ptr reader; - std::unique_ptr audiodev; + std::unique_ptr audiosrc; std::unique_ptr ui; // window pointer - QList devices; + QList devices; }; #endif // MAINWINDOW_H diff --git a/mainwindow.ui b/mainwindow.ui index 8894257..5559dc3 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -62,9 +62,6 @@ - - - @@ -75,9 +72,6 @@ - - - @@ -114,6 +108,23 @@ + + + + 99999 + + + + + + + 20 + + + 1 + + + @@ -122,7 +133,7 @@ 0 0 254 - 30 + 37 diff --git a/reader.cpp b/reader.cpp index 31e5c33..307716b 100644 --- a/reader.cpp +++ b/reader.cpp @@ -7,7 +7,7 @@ LslPusher::LslPusher(lsl::stream_outlet &&outlet) : out(std::move(outlet)), sample_bytes(out.info().sample_bytes()), cf(out.info().channel_format()) {} -qint64 LslPusher::writeData(const char* data, qint64 maxSize) +qint64 LslPusher::writeData(const char* data, const qint64 maxSize) { // qInfo() << "Write " << maxSize << ' ' << (maxSize/sample_bytes) << ' ' << (maxSize%sample_bytes); switch(cf) { diff --git a/reader.h b/reader.h index 2636563..03b80df 100644 --- a/reader.h +++ b/reader.h @@ -7,10 +7,10 @@ class LslPusher : public QIODevice { public: - LslPusher(lsl::stream_outlet &&outlet); + explicit LslPusher(lsl::stream_outlet &&outlet); qint64 writeData(const char *data, qint64 maxSize) override; - qint64 readData(char*, qint64 maxSize) override { return maxSize;} - qint64 samples_written() const { return pos() / sample_bytes; } + qint64 readData(char*, const qint64 maxSize) override { return maxSize;} + [[nodiscard]] qint64 samples_written() const { return pos() / sample_bytes; } private: lsl::stream_outlet out; From 0a150acfcfd90c613a63cf721c6a665c993bcced Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 15 May 2025 23:54:44 -0400 Subject: [PATCH 02/19] Small QoL fixes --- mainwindow.cpp | 37 ++++++++++++++++++++++--------------- mainwindow.h | 10 +++++----- mainwindow.ui | 8 ++++---- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/mainwindow.cpp b/mainwindow.cpp index a9a7697..5594ac7 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -78,7 +78,7 @@ MainWindow::MainWindow(QWidget *parent, const char *config_file) // audio devices for (const auto &dev : devices) ui->input_device->addItem(dev.description()); - auto changeSignal = static_cast(&QComboBox::currentIndexChanged); + const auto changeSignal = static_cast(&QComboBox::currentIndexChanged); connect(ui->input_device, changeSignal, this, &MainWindow::deviceChanged); deviceChanged(); @@ -87,14 +87,14 @@ MainWindow::MainWindow(QWidget *parent, const char *config_file) QString cfgfilepath = find_config_file(config_file); load_config(cfgfilepath); - checkAudioFormat(); + checkAudioFormat(cfgfilepath.length() > 0); } QAudioDevice MainWindow::currentDevice() const { return devices.at(ui->input_device->currentIndex()); } -void MainWindow::deviceChanged() { +void MainWindow::deviceChanged() const { auto const dev = currentDevice(); // Channel count @@ -105,13 +105,19 @@ void MainWindow::deviceChanged() { ui->input_samplerate->setMinimum(dev.minimumSampleRate()); ui->input_samplerate->setMaximum(dev.maximumSampleRate()); ui->input_samplerate->setValue(dev.preferredFormat().sampleRate()); + ui->input_samplerate->setToolTip( + QString("%1 - %2 Hz") + .arg(dev.minimumSampleRate()) + .arg(dev.maximumSampleRate()) + ); // Not showing up? + ui->input_samplerate->setToolTipDuration(5000); // Sample format - ui->input_samplesize->clear(); + ui->input_sampleformat->clear(); for (auto samp_fmt : dev.supportedSampleFormats()) { - ui->input_samplesize->addItem(sampleFormatToString(samp_fmt), QVariant::fromValue(samp_fmt)); + ui->input_sampleformat->addItem(sampleFormatToString(samp_fmt), QVariant::fromValue(samp_fmt)); } auto const fmt(dev.preferredFormat()); - if (const auto idx = ui->input_samplesize->findData(QVariant::fromValue(fmt.sampleFormat())); idx >= 0) ui->input_samplesize->setCurrentIndex(idx); + if (const auto idx = ui->input_sampleformat->findData(QVariant::fromValue(fmt.sampleFormat())); idx >= 0) ui->input_sampleformat->setCurrentIndex(idx); setFmt(fmt); } @@ -123,29 +129,30 @@ QAudioFormat MainWindow::selectedAudioFormat() const { fmt.setChannelCount(ui->input_channels->value()); // fmt.setByteOrder(QAudioFormat::LittleEndian); // fmt.setChannelConfig(??); - fmt.setSampleFormat(stringToSampleFormat(ui->input_samplesize->currentText())); + fmt.setSampleFormat(stringToSampleFormat(ui->input_sampleformat->currentText())); return fmt; } -void MainWindow::setFmt(const QAudioFormat &fmt) { +void MainWindow::setFmt(const QAudioFormat &fmt) const { qInfo() << "Setting fmt: " << fmt; ui->input_samplerate->setValue(fmt.sampleRate()); - ui->input_samplesize->setCurrentText(sampleFormatToString(fmt.sampleFormat())); + ui->input_sampleformat->setCurrentText(sampleFormatToString(fmt.sampleFormat())); ui->input_channels->setValue(fmt.channelCount()); - auto fmtStr = QStringLiteral("%1 channels, %2 bit @ %3 Hz") + const auto fmtStr = QStringLiteral("%1 channels, %2 bit @ %3 Hz") .arg(fmt.channelCount()) .arg(fmt.sampleFormat()) .arg(fmt.sampleRate()); ui->label_fmtresult->setText(fmtStr); } -void MainWindow::checkAudioFormat() { +void MainWindow::checkAudioFormat(const bool showModal = true) { auto fmt = selectedAudioFormat(); if (const auto dev = currentDevice(); dev.isFormatSupported(fmt)) qInfo() << "Format is supported"; else { - QMessageBox::warning(this, "Format not supported", - "The requested format isn't supported; the preferred format was automatically selected."); + if (showModal) + QMessageBox::warning(this, "Format not supported", + "The requested format isn't supported; the preferred format was automatically selected."); fmt = dev.preferredFormat(); } setFmt(fmt); @@ -157,7 +164,7 @@ void MainWindow::load_config(const QString &filename) const { ui->input_device->setCurrentIndex(settings.value("AudioCapture/device", 0).toInt()); ui->input_samplerate->setValue(settings.value("AudioCapture/samplerate", 1).toInt()); const QString sampleSize = settings.value("AudioCapture/samplesize", "Int16").toString(); - ui->input_samplesize->setCurrentIndex(ui->input_samplesize->findData(QVariant::fromValue(sampleSize))); + ui->input_sampleformat->setCurrentIndex(ui->input_sampleformat->findData(QVariant::fromValue(sampleSize))); ui->input_channels->setValue(settings.value("AudioCapture/channels", 0).toInt()); } @@ -167,7 +174,7 @@ void MainWindow::save_config(const QString &filename) const { settings.setValue("name", ui->input_name->text()); settings.setValue("device", ui->input_device->currentIndex()); settings.setValue("samplerate", ui->input_samplerate->value()); - settings.setValue("samplesize", ui->input_samplesize->currentText()); + settings.setValue("samplesize", ui->input_sampleformat->currentText()); settings.setValue("channels", ui->input_channels->value()); settings.sync(); } diff --git a/mainwindow.h b/mainwindow.h index acb36d0..56d2a43 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -19,14 +19,14 @@ class MainWindow : public QMainWindow { private slots: void closeEvent(QCloseEvent *ev) override; void toggleRecording(); - void deviceChanged(); - void checkAudioFormat(); + void deviceChanged() const; + void checkAudioFormat(bool showModal); private: // Audio device handling - QAudioDevice currentDevice() const; - void setFmt(const QAudioFormat &fmt); - QAudioFormat selectedAudioFormat() const; + [[nodiscard]] QAudioDevice currentDevice() const; + void setFmt(const QAudioFormat &fmt) const; + [[nodiscard]] QAudioFormat selectedAudioFormat() const; // void updateSampleRates(); // function for loading / saving the config file diff --git a/mainwindow.ui b/mainwindow.ui index 5559dc3..6071048 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -63,14 +63,14 @@ - + - Sample Size + Sample Format - + @@ -133,7 +133,7 @@ 0 0 254 - 37 + 24 From d216a150696b53dc9d435b11c605506f301cbb3e Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Thu, 31 Jul 2025 22:02:57 -0600 Subject: [PATCH 03/19] Qt5->Qt6 fixes in README --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4f59a5c..d09a689 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Overview -The AudioCapture application uses Qt's [QAudioInput](https://doc.qt.io/qt-5/qaudioinput.html) for cross-platform audio capturing. +The AudioCapture application uses Qt's [QAudioSource](https://doc.qt.io/qt-6/qaudiosource.html) for cross-platform audio capturing. This program has been tested on Windows and MacOS. Download the latest version [from the releases page](https://github.com/labstreaminglayer/App-AudioCapture/releases). The Windows release requires vc_redist.x64.exe [from Microsoft](https://support.microsoft.com/en-gb/help/2977003/the-latest-supported-visual-c-downloads). @@ -9,7 +9,7 @@ The Windows release requires vc_redist.x64.exe [from Microsoft](https://support. Using this app is very simple: * Make sure that you have connected a microphone to your computer. - * On Ubuntu, you need to `sudo apt-get install libqt5multimedia5-plugins` + * On Ubuntu, you need to `sudo apt-get install libqt6multimedia6` * Start the AudioCapture app. You should see a window like the following. > ![audiocapture.PNG](audiocapture.PNG) * Set the audio capture parameters. @@ -23,8 +23,12 @@ Using this app is very simple: # Build -The build instructions for this app are mostly the same as the [generic LSL App build instructions](https://labstreaminglayer.readthedocs.io/dev/app_build.html). -Qt Multimedia module is required. On Ubuntu this does not come with qt5 by default. Install it with `sudo apt-get install qtmultimedia5-dev`. On Mac, with homebrew, it is only included in qt6 >= 6.2, which isn't out yet, so qt5 is required. +The build instructions for this app are mostly the same as the [generic Qt-based LSL App build instructions](https://labstreaminglayer.readthedocs.io/dev/app_build.html). +Additionally, Qt Multimedia development libraries are required: + +* On Ubuntu, install it with `sudo apt-get install qt6-multimedia-dev`. +* On Mac (using homebrew) or Windows, the multimedia libraries should come with the Qt6 installation. + On Mac, it appears to be necessary to use the Xcode generator: `cmake -B build -S . -DCMAKE_INSTALL_PREFIX="build/install" -G Xcode` Note that code-signing has been disabled. From ca97f1a41074208ef87ea8d9d214dcc49a60a250 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Sun, 3 Aug 2025 15:02:34 -0400 Subject: [PATCH 04/19] WIP - Fixup build and deployment using lsl.framework. --- .gitignore | 1 + CMakeLists.txt | 131 ++++++++++++++++++++++++++++++++----------------- 2 files changed, 88 insertions(+), 44 deletions(-) diff --git a/.gitignore b/.gitignore index 59498ed..890e859 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ ui_*.h /build*/ +/install*/ /CMakeLists.txt.user /CMakeLists.json /CMakeSettings.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 1cd903c..8b5b933 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.12) +cmake_minimum_required(VERSION 3.25) set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum MacOS deployment version") project(AudioCapture @@ -13,24 +13,9 @@ SET(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake" ${CMAKE_MODULE_PATH}) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) - ## Qt -set(CMAKE_AUTOMOC ON) # The later version of this in LSLCMake is somehow not enough. -set(CMAKE_AUTORCC ON) -set(CMAKE_AUTOUIC ON) -find_package(Qt6 COMPONENTS Core Widgets Network DBus Multimedia) # Multimedia requires Qt6.2 -if(NOT Qt6_FOUND) - if(APPLE) - list(APPEND CMAKE_PREFIX_PATH "/usr/local/opt/qt@5") - endif() - # If we require 5.15 then we can use version-agnostic linking, but 5.15 not easily available on Ubuntu. - find_package(Qt5 COMPONENTS Core Widgets Network DBus Multimedia REQUIRED) - add_executable(${PROJECT_NAME} MACOSX_BUNDLE) - set(LSLAPP_QT_VER Qt5) -else() - qt_add_executable(${PROJECT_NAME} MACOSX_BUNDLE MANUAL_FINALIZATION) - set(LSLAPP_QT_VER Qt) -endif() +find_package(Qt6 REQUIRED COMPONENTS Core Widgets Network DBus Multimedia) +qt_standard_project_setup() # LSL find_package(LSL REQUIRED @@ -41,9 +26,13 @@ find_package(LSL REQUIRED "${CMAKE_CURRENT_LIST_DIR}/../../LSL/liblsl/out/install/x64-Release" PATH_SUFFIXES share/LSL) get_filename_component(LSL_PATH ${LSL_CONFIG} DIRECTORY) +include("${LSL_PATH}/LSLCMake.cmake") find_package(Threads REQUIRED) +# Create the target +qt_add_executable(${PROJECT_NAME} MACOSX_BUNDLE MANUAL_FINALIZATION) +set(MACOSX_BUNDLE_GUI_IDENTIFIER "com.labstreaminglayer.AudioCapture") target_sources(${PROJECT_NAME} PRIVATE main.cpp mainwindow.cpp @@ -55,39 +44,93 @@ target_sources(${PROJECT_NAME} PRIVATE target_link_libraries(${PROJECT_NAME} PRIVATE - ${LSLAPP_QT_VER}::Widgets - ${LSLAPP_QT_VER}::Multimedia + Qt::Widgets + Qt::Multimedia + Qt::DBus Threads::Threads LSL::lsl ) -set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO") -set_target_properties(${PROJECT_NAME} PROPERTIES - XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO" -) +if(APPLE) + # Code signing is not required for debugging, and our deployment script will handle signing for release builds. + set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO") + set_target_properties(${PROJECT_NAME} PROPERTIES + XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO" + ) +endif(APPLE) -# Copy the required dll's into the build folder --> useful for debugging from IDE -# create a list of files to copy -set(THIRD_PARTY_DLLS - LSL::lsl - ${LSLAPP_QT_VER}::Core - ${LSLAPP_QT_VER}::Gui - ${LSLAPP_QT_VER}::Widgets - ${LSLAPP_QT_VER}::Multimedia - ${LSLAPP_QT_VER}::Network +if(WIN32) + # Copy the required dll's into the build folder --> useful for debugging from IDE + # create a list of files to copy + set(THIRD_PARTY_DLLS + LSL::lsl + Qt::Core + Qt::Gui + Qt::Widgets + Qt::DBus + Qt::Multimedia + Qt::Network + ) + foreach(_lib ${THIRD_PARTY_DLLS}) + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $) + endforeach() +endif(WIN32) + +qt_finalize_target(${PROJECT_NAME}) + +install( + TARGETS ${PROJECT_NAME} + COMPONENT ${PROJECT_NAME} + BUNDLE DESTINATION . + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} ) -foreach(_lib ${THIRD_PARTY_DLLS}) - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $) -endforeach() +if(APPLE) + # Install LSL framework into bundle + get_filename_component(LSL_RESOURCES_DIR ${LSL_PATH} DIRECTORY) + get_filename_component(LSL_FRAMEWORK_DIR ${LSL_RESOURCES_DIR} DIRECTORY) + install(DIRECTORY "${LSL_FRAMEWORK_DIR}" + DESTINATION "${PROJECT_NAME}.app/Contents/Frameworks" + COMPONENT ${PROJECT_NAME}) + + # The following should already be part of the INSTALL_RPATH so we don't need to append it. + # set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY INSTALL_RPATH "@executable_path/../Frameworks") -if(Qt6_FOUND) - set(MACOSX_BUNDLE_GUI_IDENTIFIER "com.labstreaminglayer.AudioCapture") - qt_finalize_executable(${PROJECT_NAME}) + # Additionally, the qt_generate_deploy_app_script() will remove /Library/Frameworks from the rpath, + # meaning that we don't have to worry about the LSL framework being found in the system paths. + # i.e., it's OK that the install name for lsl.framework is set to @rpath/lsl.framework/Versions/A/lsl. + # and not to @executable_path/../Frameworks/lsl.framework/Versions/A/lsl. endif() -installLSLApp(${PROJECT_NAME}) -LSLGenerateCPackConfig() +qt_generate_deploy_app_script( + TARGET ${PROJECT_NAME} + OUTPUT_SCRIPT deploy_script_path + NO_UNSUPPORTED_PLATFORM_ERROR +) +install(SCRIPT ${deploy_script_path}) + +if(APPLE) + # Add a post-install step to codesign the app bundle, but only if the APPLE_CODE_SIGN_IDENTITY_APP is set. + install(CODE " + if(NOT \"$ENV{APPLE_CODE_SIGN_IDENTITY_APP}\" STREQUAL \"\") + message(STATUS \"Codesigning app bundle with identity: $ENV{APPLE_CODE_SIGN_IDENTITY_APP}\") + execute_process( + COMMAND codesign -vvv --force --deep --sign \"$ENV{APPLE_CODE_SIGN_IDENTITY_APP}\" --options runtime \"${CMAKE_INSTALL_PREFIX}/${PROJECT_NAME}.app\" + RESULT_VARIABLE result + ) + if(NOT result EQUAL 0) + message(FATAL_ERROR \"codesign failed for ${PROJECT_NAME}.app with exit code ${result}\") + endif() + endif() + " COMPONENT ${PROJECT_NAME}) +endif() + +#installLSLApp(${PROJECT_NAME}) + +# TODO: Generate CPack config file for DMG +#LSLGenerateCPackConfig() From 7cba96e88f773626b503e540fd64f0abeab37588 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Sun, 3 Aug 2025 23:51:07 -0400 Subject: [PATCH 05/19] Add a couple more 'default' search paths for liblsl --- CMakeLists.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b5b933..5a9552f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,8 @@ find_package(LSL REQUIRED HINTS ${LSL_INSTALL_ROOT} "${CMAKE_CURRENT_LIST_DIR}/../../LSL/liblsl/build/" "${CMAKE_CURRENT_LIST_DIR}/../../LSL/liblsl/build/install" + "${CMAKE_CURRENT_LIST_DIR}/../../LSL/liblsl/cmake-build-release/install" + "${CMAKE_CURRENT_LIST_DIR}/../../LSL/liblsl/cmake-build-release-visual-studio/install" "${CMAKE_CURRENT_LIST_DIR}/../../LSL/liblsl/out/build/x64-Release" "${CMAKE_CURRENT_LIST_DIR}/../../LSL/liblsl/out/install/x64-Release" PATH_SUFFIXES share/LSL) From 3d694e98e5d944b0830d714b81f958b75d5419eb Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Sun, 3 Aug 2025 23:51:43 -0400 Subject: [PATCH 06/19] Ignore .idea --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 890e859..2de315a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ out/ .DS_Store LSL/ liblsl.tar.bz2 - +.idea/ From b17e0b7b81c4cc708838e0d0a5f55632a3cf0fad Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 4 Aug 2025 00:03:50 -0400 Subject: [PATCH 07/19] On Windows, copy liblsl.dll into 'install' --- CMakeLists.txt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5a9552f..6df81be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,7 +34,7 @@ find_package(Threads REQUIRED) # Create the target qt_add_executable(${PROJECT_NAME} MACOSX_BUNDLE MANUAL_FINALIZATION) -set(MACOSX_BUNDLE_GUI_IDENTIFIER "com.labstreaminglayer.AudioCapture") +set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.labstreaminglayer.AudioCapture") target_sources(${PROJECT_NAME} PRIVATE main.cpp mainwindow.cpp @@ -63,6 +63,7 @@ endif(APPLE) if(WIN32) # Copy the required dll's into the build folder --> useful for debugging from IDE + # TODO: More to be done: "The application failed to start because no Qt platform plugin could be initialized." # create a list of files to copy set(THIRD_PARTY_DLLS LSL::lsl @@ -92,6 +93,13 @@ install( ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} ) +if(WIN32) + install(FILES $ + DESTINATION ${CMAKE_INSTALL_BINDIR} + COMPONENT ${PROJECT_NAME} + ) +endif() + if(APPLE) # Install LSL framework into bundle get_filename_component(LSL_RESOURCES_DIR ${LSL_PATH} DIRECTORY) From 9c1ee18f40c53af39d04f561b4798e7f095363d4 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 4 Aug 2025 00:24:45 -0400 Subject: [PATCH 08/19] Fixup copying deps into build folder on Windows -- necessary for in-IDE debugging. --- CMakeLists.txt | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6df81be..4e967b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,24 +62,28 @@ if(APPLE) endif(APPLE) if(WIN32) - # Copy the required dll's into the build folder --> useful for debugging from IDE - # TODO: More to be done: "The application failed to start because no Qt platform plugin could be initialized." - # create a list of files to copy - set(THIRD_PARTY_DLLS - LSL::lsl - Qt::Core - Qt::Gui - Qt::Widgets - Qt::DBus - Qt::Multimedia - Qt::Network + # Copy all required DLLs into the build folder --> useful for debugging from IDE + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ + COMMAND_EXPAND_LISTS + COMMENT "Copying runtime dependencies for ${PROJECT_NAME}" ) - foreach(_lib ${THIRD_PARTY_DLLS}) - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $) - endforeach() + get_target_property(_qmake_executable Qt6::qmake IMPORTED_LOCATION) + execute_process( + COMMAND ${_qmake_executable} -query QT_INSTALL_PLUGINS + OUTPUT_VARIABLE QT_PLUGINS_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${QT_PLUGINS_DIR}/platforms" + "$/platforms") + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${QT_PLUGINS_DIR}/styles" + "$/styles") endif(WIN32) qt_finalize_target(${PROJECT_NAME}) From ba93a7b34a0a11673e9da25a6c778b4509fd424d Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 4 Aug 2025 00:28:17 -0400 Subject: [PATCH 09/19] README touchup before switching platforms --- README.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d09a689..f3e90e7 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,25 @@ # Overview The AudioCapture application uses Qt's [QAudioSource](https://doc.qt.io/qt-6/qaudiosource.html) for cross-platform audio capturing. -This program has been tested on Windows and MacOS. Download the latest version [from the releases page](https://github.com/labstreaminglayer/App-AudioCapture/releases). +This program has been tested on Windows and MacOS. The Windows release requires vc_redist.x64.exe [from Microsoft](https://support.microsoft.com/en-gb/help/2977003/the-latest-supported-visual-c-downloads). -# Usage +# Getting Started + +Download the latest version [from the releases page](https://github.com/labstreaminglayer/App-AudioCapture/releases). + +## Extra Dependencies (Ubuntu only) + +* `sudo apt-get install libqt6multimedia6` +* TODO: Instructions to download and install liblsl +* Other platforms ship with the Qt libraries. + +## Usage + Using this app is very simple: * Make sure that you have connected a microphone to your computer. - * On Ubuntu, you need to `sudo apt-get install libqt6multimedia6` * Start the AudioCapture app. You should see a window like the following. > ![audiocapture.PNG](audiocapture.PNG) * Set the audio capture parameters. @@ -19,20 +29,20 @@ Using this app is very simple: * Click the "Link" button to link the app to the lab network. If successful, the button should turn into "Unlink". * If a firewall complains, allow the app to connect to the network. * Please allow microphone access if asked. - * You should now have a stream on your lab network that has type "Audio" and its name is the name entered in the GUI. Note that you cannot close the app while it is linked. + * You should now have a stream on your lab network that has type "Audio" and its name is the name entered in the GUI. + * Note that you cannot close the app while it is linked. # Build The build instructions for this app are mostly the same as the [generic Qt-based LSL App build instructions](https://labstreaminglayer.readthedocs.io/dev/app_build.html). + Additionally, Qt Multimedia development libraries are required: * On Ubuntu, install it with `sudo apt-get install qt6-multimedia-dev`. -* On Mac (using homebrew) or Windows, the multimedia libraries should come with the Qt6 installation. +* On other platforms, the Qt Multimedia libraries should be included in the Qt installation. On Mac, it appears to be necessary to use the Xcode generator: `cmake -B build -S . -DCMAKE_INSTALL_PREFIX="build/install" -G Xcode` -Note that code-signing has been disabled. - # Further Notes The previous version of AudioCaptureWin can be found as [release v0.1](https://github.com/labstreaminglayer/App-AudioCapture/releases/tag/v0.1) in this repository. From 802e439b264fa515ec56683f379c67f1458e2123 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 4 Aug 2025 14:06:07 -0400 Subject: [PATCH 10/19] Qt install - no translations to suppress warnings. --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e967b5..bd3a48b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -124,6 +124,7 @@ endif() qt_generate_deploy_app_script( TARGET ${PROJECT_NAME} OUTPUT_SCRIPT deploy_script_path + NO_TRANSLATIONS NO_UNSUPPORTED_PLATFORM_ERROR ) install(SCRIPT ${deploy_script_path}) From 42a1a3bbca78f05ab434a895f7464a9ab0906d84 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 4 Aug 2025 14:06:24 -0400 Subject: [PATCH 11/19] Update scaffolding for build notes. --- README.md | 59 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index f3e90e7..6960c14 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,12 @@ # Overview -The AudioCapture application uses Qt's [QAudioSource](https://doc.qt.io/qt-6/qaudiosource.html) for cross-platform audio capturing. -This program has been tested on Windows and MacOS. - -The Windows release requires vc_redist.x64.exe [from Microsoft](https://support.microsoft.com/en-gb/help/2977003/the-latest-supported-visual-c-downloads). +The AudioCapture application uses Qt's [QAudioSource](https://doc.qt.io/qt-6/qaudiosource.html) for cross-platform audio capturing and streaming over [LSL](https://labstreaminglayer.org). # Getting Started Download the latest version [from the releases page](https://github.com/labstreaminglayer/App-AudioCapture/releases). -## Extra Dependencies (Ubuntu only) - -* `sudo apt-get install libqt6multimedia6` -* TODO: Instructions to download and install liblsl -* Other platforms ship with the Qt libraries. - -## Usage +The Windows release requires vc_redist.x64.exe; if you don't already have it then you can install the download [from Microsoft](https://support.microsoft.com/en-gb/help/2977003/the-latest-supported-visual-c-downloads). Using this app is very simple: @@ -32,16 +23,50 @@ Using this app is very simple: * You should now have a stream on your lab network that has type "Audio" and its name is the name entered in the GUI. * Note that you cannot close the app while it is linked. -# Build +# Build from source + +## Pre-requisites + +### liblsl + +TODO + +### Qt6 >= 6.5 + +* MacOS: `brew install qt` +* Windows or Linux: Download and run installer + * You will need a (free) Qt account + * This is an open source project so you can use the LGPL Qt6 open source version + * QtMultimedia should be enabled by default so default options are fine + +### Build Essentials + +* CMake >= 3.25 +* Compiler + +## Instructions + +** Configure: ** + * MacOS: `cmake -B build -S . -DCMAKE_INSTALL_PREFIX=build/install -DCMAKE_BUILD_TYPE=Release -G Xcode` + * Linux: `cmake -B build -S . -DCMAKE_INSTALL_PREFIX=build/install -DCMAKE_BUILD_TYPE=Release -DQt6_DIR=~/Qt/6.9.1/gcc_64/lib/cmake/Qt6` + * Windows: `cmake -B build -S . -DCMAKE_INSTALL_PREFIX=build/install -DCMAKE_BUILD_TYPE=Release -DQt6_DIR=C:\\Qt\\6.9.1\\mingw_64\\lib\\cmake\\Qt6` + +** Build: ** + * `cmake --build build -DCMAKE_BUILD_TYPE=Release --target install` + +Note on MacOS: If the `APPLE_CODE_SIGN_IDENTITY_APP` env variable is set then the package will be code-signed at this stage. + +** Package: ** + +TODO -The build instructions for this app are mostly the same as the [generic Qt-based LSL App build instructions](https://labstreaminglayer.readthedocs.io/dev/app_build.html). +** Notarization (MacOS only; Optional): ** -Additionally, Qt Multimedia development libraries are required: +TODO -* On Ubuntu, install it with `sudo apt-get install qt6-multimedia-dev`. -* On other platforms, the Qt Multimedia libraries should be included in the Qt installation. +** Deploy and Use: ** -On Mac, it appears to be necessary to use the Xcode generator: `cmake -B build -S . -DCMAKE_INSTALL_PREFIX="build/install" -G Xcode` +Without the packaging step in place, the build/install/ folder will container # Further Notes From 23c06afdb8c0e10cf4c078fbf89c49e7cd99402e Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 9 Jan 2026 18:46:05 -0500 Subject: [PATCH 12/19] More build fixes --- .github/workflows/build.yml | 257 +++++++++++ .github/workflows/cppcmake.yml | 103 ----- CMakeLists.txt | 435 +++++++++++++----- app.entitlements | 21 + ...leInfo.plist => MacOSXBundleInfo.plist.in} | 0 scripts/sign_and_notarize.sh | 107 +++++ 6 files changed, 715 insertions(+), 208 deletions(-) create mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/cppcmake.yml create mode 100644 app.entitlements rename cmake/{MacOSXBundleInfo.plist => MacOSXBundleInfo.plist.in} (100%) create mode 100755 scripts/sign_and_notarize.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..df941eb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,257 @@ +# ============================================================================= +# AudioCapture Build Workflow +# ============================================================================= +# This workflow builds, tests, and packages AudioCapture for all +# supported platforms. +# +# Features: +# - Multi-platform builds (Linux, macOS, Windows) +# - Qt6 integration +# - Automatic liblsl fetch via FetchContent +# - CPack packaging +# - macOS code signing and notarization (on release) +# ============================================================================= + +name: Build + +on: + push: + branches: [main, master] + tags: ['v*'] + pull_request: + branches: [main, master] + release: + types: [published] + workflow_dispatch: + +env: + BUILD_TYPE: Release + +jobs: + # =========================================================================== + # Build Job - Multi-platform builds + # =========================================================================== + build: + name: ${{ matrix.config.name }} + runs-on: ${{ matrix.config.os }} + strategy: + fail-fast: false + matrix: + config: + - { name: "Ubuntu 22.04", os: ubuntu-22.04 } + - { name: "Ubuntu 24.04", os: ubuntu-24.04 } + - { name: "macOS", os: macos-14, cmake_extra: '-DCMAKE_OSX_ARCHITECTURES="x86_64;arm64"' } + - { name: "Windows", os: windows-latest } + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # ----------------------------------------------------------------------- + # Install CMake 3.28+ (Ubuntu 22.04 ships with 3.22) + # ----------------------------------------------------------------------- + - name: Install CMake + if: runner.os == 'Linux' + uses: lukka/get-cmake@latest + + # ----------------------------------------------------------------------- + # Install Qt6 (6.8 LTS across all platforms) + # ----------------------------------------------------------------------- + - name: Install Linux dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libgl1-mesa-dev libxkbcommon-dev libxcb-cursor0 \ + libasound2-dev libpulse-dev + + - name: Install Qt + uses: jurplel/install-qt-action@v4 + with: + version: '6.8.*' + modules: 'qtmultimedia' + cache: true + + # ----------------------------------------------------------------------- + # Configure + # ----------------------------------------------------------------------- + - name: Configure CMake + run: > + cmake -S . -B build + -DCMAKE_BUILD_TYPE=${{ env.BUILD_TYPE }} + -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/install + -DLSL_FETCH_IF_MISSING=ON + ${{ matrix.config.cmake_extra }} + + # ----------------------------------------------------------------------- + # Build + # ----------------------------------------------------------------------- + - name: Build + run: cmake --build build --config ${{ env.BUILD_TYPE }} --parallel + + # ----------------------------------------------------------------------- + # Install + # ----------------------------------------------------------------------- + - name: Install + run: cmake --install build --config ${{ env.BUILD_TYPE }} + + # ----------------------------------------------------------------------- + # Package + # ----------------------------------------------------------------------- + - name: Package + run: cpack -C ${{ env.BUILD_TYPE }} + working-directory: build + + # ----------------------------------------------------------------------- + # Upload Artifacts + # ----------------------------------------------------------------------- + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: package-${{ matrix.config.os }} + path: | + build/*.zip + build/*.tar.gz + build/*.deb + if-no-files-found: ignore + + # =========================================================================== + # macOS Signing and Notarization (Release only) + # =========================================================================== + sign-macos: + name: Sign & Notarize (macOS) + needs: build + if: github.event_name == 'release' + runs-on: macos-14 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Download macOS Artifact + uses: actions/download-artifact@v4 + with: + name: package-macos-14 + path: packages + + - name: Extract Package + run: | + cd packages + tar -xzf *.tar.gz + # Move contents out of versioned subdirectory to packages/ + SUBDIR=$(ls -d AudioCapture-*/ | head -1) + mv "$SUBDIR"/* . + rmdir "$SUBDIR" + ls -la + + # ----------------------------------------------------------------------- + # Install Apple Certificates + # ----------------------------------------------------------------------- + - name: Install Apple Certificates + env: + MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }} + MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }} + run: | + # Create temporary keychain + KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain + security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" $KEYCHAIN_PATH + security default-keychain -s $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" $KEYCHAIN_PATH + + # Import certificate + CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 + echo -n "$MACOS_CERTIFICATE" | base64 --decode -o $CERTIFICATE_PATH + security import $CERTIFICATE_PATH -P "$MACOS_CERTIFICATE_PWD" -k $KEYCHAIN_PATH -A -t cert -f pkcs12 + rm $CERTIFICATE_PATH + + # Allow codesign to access keychain + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + + # Extract identity name and export to environment + IDENTITY=$(security find-identity -v -p codesigning $KEYCHAIN_PATH | grep "Developer ID Application" | head -1 | awk -F'"' '{print $2}') + echo "APPLE_CODE_SIGN_IDENTITY_APP=$IDENTITY" >> $GITHUB_ENV + + # ----------------------------------------------------------------------- + # Setup Notarization Credentials + # ----------------------------------------------------------------------- + - name: Setup Notarization + env: + NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} + NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} + NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} + run: | + xcrun notarytool store-credentials "notarize-profile" \ + --apple-id "$NOTARIZATION_APPLE_ID" \ + --password "$NOTARIZATION_PWD" \ + --team-id "$NOTARIZATION_TEAM_ID" + echo "APPLE_NOTARIZE_KEYCHAIN_PROFILE=notarize-profile" >> $GITHUB_ENV + + # ----------------------------------------------------------------------- + # Sign and Notarize + # ----------------------------------------------------------------------- + - name: Sign and Notarize + env: + ENTITLEMENTS_FILE: ${{ github.workspace }}/app.entitlements + run: | + APP_PATH=$(find packages -name "*.app" -type d | head -1) + if [[ -n "$APP_PATH" ]]; then + ./scripts/sign_and_notarize.sh "$APP_PATH" --notarize + fi + + # ----------------------------------------------------------------------- + # Repackage + # ----------------------------------------------------------------------- + - name: Repackage + run: | + cd packages + + echo "Contents of packages directory:" + ls -la + + rm -f *.tar.gz + + VERSION=$(grep -A1 'project(AudioCapture' ../CMakeLists.txt | grep VERSION | sed 's/.*VERSION \([0-9.]*\).*/\1/') + echo "Detected version: $VERSION" + + tar -cvzf "AudioCapture-${VERSION}-macOS_universal-signed.tar.gz" \ + AudioCapture.app + + echo "Created package:" + ls -la *.tar.gz + + - name: Upload Signed Package + uses: actions/upload-artifact@v4 + with: + name: package-macos-signed + path: packages/*-signed.tar.gz + + - name: Upload to Release + if: github.event_name == 'release' + uses: softprops/action-gh-release@v2 + with: + files: packages/*-signed.tar.gz + + # =========================================================================== + # Upload unsigned packages to release + # =========================================================================== + release: + name: Upload to Release + needs: build + if: github.event_name == 'release' + runs-on: ubuntu-latest + + steps: + - name: Download All Artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Upload to Release + uses: softprops/action-gh-release@v2 + with: + files: | + artifacts/**/*.zip + artifacts/**/*.tar.gz + artifacts/**/*.deb diff --git a/.github/workflows/cppcmake.yml b/.github/workflows/cppcmake.yml deleted file mode 100644 index cc5854b..0000000 --- a/.github/workflows/cppcmake.yml +++ /dev/null @@ -1,103 +0,0 @@ -name: C/C++ CI - -on: - push: - tags: - - v*.* - pull_request: - branches: - - master - -env: - LSL_URL: 'https://github.com/sccn/liblsl/releases/download' - LSL_RELEASE_PREFIX: 'v' - LSL_RELEASE: '1.14.0' - LSL_RELEASE_SUFFIX: 'b1' - - -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: - - ubuntu-latest - - windows-latest - - macOS-latest - fail-fast: false - - steps: - - uses: actions/checkout@v2 - - - name: Install Qt (Windows) - if: matrix.os == 'windows-latest' - uses: jurplel/install-qt-action@v2 - with: - version: 5.14.0 - - - name: Get liblsl (Windows) - if: matrix.os == 'windows-latest' - run: | - Invoke-WebRequest -Uri $Env:LSL_URL/$Env:LSL_RELEASE_PREFIX$Env:LSL_RELEASE$Env:LSL_RELEASE_SUFFIX/liblsl-$Env:LSL_RELEASE-Win64.zip -o liblsl.7z - 7z x liblsl.7z -oLSL - - - name: Get liblsl and Qt (Ubuntu) - if: matrix.os == 'ubuntu-latest' - run: | - echo ${{ github.ref }} - curl -L ${LSL_URL}/${LSL_RELEASE_PREFIX}${LSL_RELEASE}${LSL_RELEASE_SUFFIX}/liblsl-${LSL_RELEASE}-Linux64-bionic.deb -o liblsl.deb - sudo dpkg -i liblsl.deb - sudo apt install -y qtbase5-dev qtmultimedia5-dev - - - name: Get liblsl and Qt (macOS) - if: matrix.os == 'macOS-latest' - run: | - curl -L ${LSL_URL}/${LSL_RELEASE_PREFIX}${LSL_RELEASE}${LSL_RELEASE_SUFFIX}/liblsl-${LSL_RELEASE}-OSX64.tar.bz2 -o liblsl.tar.bz2 - mkdir LSL - tar -xvf liblsl.tar.bz2 -C LSL - brew install qt - echo '::set-env name=CMAKE_PREFIX_PATH::/usr/local/opt/qt' - - - name: Configure CMake - shell: bash - run: | - cmake -S . -B build -DLSL_INSTALL_ROOT=$PWD/LSL/ -DCPACK_DEBIAN_PACKAGE_SHLIBDEPS=ON -DCPACK_DEBIAN_PACKAGE_DEPENDS=1 - - - name: Make & Install - run: cmake --build build --config Release -j --target install - - - name: Package - run: cmake --build build --config Release -j --target package - - - name: Upload Artifacts - uses: actions/upload-artifact@v2-preview - with: - name: pkg-${{ matrix.os }} - path: build/*.[dbz][ezi][b2p] # Hack to get deb, bz2, zip. Will also get e.g. de2, dep, dzb, dz2, dzp, etc... - - release: - needs: build - runs-on: ubuntu-latest - steps: - - - name: Download Artifacts - if: startsWith(github.ref, 'refs/tags/') - uses: actions/download-artifact@v2-preview - # By not specifying with: name:, it defaults to downloading all artifacts. - - # Official GitHub Upload-Asset action does not allow for uploading multiple files. - # There are many community alternatives. Below is one that combines release and upload, with globbing. - # See also: svenstaro/upload-release-action shogo82148/actions-upload-release-asset meeDamian/github-release csexton/release-asset-action - - name: Create Release - if: startsWith(github.ref, 'refs/tags/') - id: create_release - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - # tag_name: ${{ github.ref }} # ${{ github.ref }} is default - name: Release ${{ github.ref }} - draft: false - prerelease: true - # body_path: CHANGELOG.txt - files: pkg-*-latest/* diff --git a/CMakeLists.txt b/CMakeLists.txt index bd3a48b..a85c750 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,40 +1,163 @@ -cmake_minimum_required(VERSION 3.25) -set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum MacOS deployment version") +# ============================================================================= +# AudioCapture - CMakeLists.txt +# ============================================================================= +# Capture audio and stream it over Lab Streaming Layer. +# ============================================================================= + +cmake_minimum_required(VERSION 3.28) + +# CMP0177: install() DESTINATION paths are normalized (CMake 3.31+) +if(POLICY CMP0177) + cmake_policy(SET CMP0177 NEW) +endif() project(AudioCapture + VERSION 0.2.0 DESCRIPTION "Capture audio and stream it over LabStreamingLayer" HOMEPAGE_URL "https://github.com/labstreaminglayer/App-AudioCapture/" LANGUAGES CXX C - VERSION 0.1) - -# Needed for customized MacOSXBundleInfo.plist.in -SET(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake" ${CMAKE_MODULE_PATH}) +) -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# ============================================================================= +# LSL Discovery Options +# ============================================================================= +# Priority 1: Build from local source (for parallel liblsl development) +set(LSL_SOURCE_DIR "" CACHE PATH "Path to liblsl source directory") + +# Priority 2: Use explicit installation path +set(LSL_INSTALL_ROOT "" CACHE PATH "Path to installed liblsl") + +# Priority 3: System installation (searched automatically) -## Qt -find_package(Qt6 REQUIRED COMPONENTS Core Widgets Network DBus Multimedia) -qt_standard_project_setup() - -# LSL -find_package(LSL REQUIRED - HINTS ${LSL_INSTALL_ROOT} - "${CMAKE_CURRENT_LIST_DIR}/../../LSL/liblsl/build/" - "${CMAKE_CURRENT_LIST_DIR}/../../LSL/liblsl/build/install" - "${CMAKE_CURRENT_LIST_DIR}/../../LSL/liblsl/cmake-build-release/install" - "${CMAKE_CURRENT_LIST_DIR}/../../LSL/liblsl/cmake-build-release-visual-studio/install" - "${CMAKE_CURRENT_LIST_DIR}/../../LSL/liblsl/out/build/x64-Release" - "${CMAKE_CURRENT_LIST_DIR}/../../LSL/liblsl/out/install/x64-Release" - PATH_SUFFIXES share/LSL) -get_filename_component(LSL_PATH ${LSL_CONFIG} DIRECTORY) -include("${LSL_PATH}/LSLCMake.cmake") +# Priority 4: Fetch from GitHub if not found +option(LSL_FETCH_IF_MISSING "Fetch liblsl from GitHub if not found locally" ON) +# TODO: Change back to version tag (e.g., "v1.16.2") once apple_framework branch is merged +set(LSL_FETCH_REF "cboulay/apple_framework" CACHE STRING "liblsl git ref to fetch (tag, branch, or commit)") +# ============================================================================= +# Find/Fetch liblsl +# ============================================================================= +if(LSL_SOURCE_DIR) + # Priority 1: Build from local source (parallel development) + if(NOT EXISTS "${LSL_SOURCE_DIR}/CMakeLists.txt") + message(FATAL_ERROR "LSL_SOURCE_DIR set to '${LSL_SOURCE_DIR}' but no CMakeLists.txt found there") + endif() + message(STATUS "Using local liblsl source: ${LSL_SOURCE_DIR}") + add_subdirectory("${LSL_SOURCE_DIR}" liblsl_bin EXCLUDE_FROM_ALL) + if(NOT TARGET LSL::lsl) + add_library(LSL::lsl ALIAS lsl) + endif() + set(LSL_FOUND TRUE) +else() + # Priority 2 & 3: Try to find installed liblsl + set(_lsl_hints) + if(LSL_INSTALL_ROOT) + list(APPEND _lsl_hints "${LSL_INSTALL_ROOT}") + endif() + # Common development layout hints + string(TOLOWER "${CMAKE_BUILD_TYPE}" _build_type_lower) + foreach(_root IN ITEMS "../liblsl" "../../LSL/liblsl") + foreach(_build IN ITEMS "build" "cmake-build-${_build_type_lower}") + list(APPEND _lsl_hints "${CMAKE_CURRENT_LIST_DIR}/${_root}/${_build}/install") + endforeach() + endforeach() + + set(_lsl_suffixes + share/LSL + lib/cmake/LSL + cmake + Frameworks/lsl.framework/Resources/CMake # macOS framework layout + ) + + # First try: Search only in hints (prefer local builds over system) + find_package(LSL QUIET + HINTS ${_lsl_hints} + PATH_SUFFIXES ${_lsl_suffixes} + NO_DEFAULT_PATH + ) + + # Second try: Search system paths if not found in hints + if(NOT LSL_FOUND) + find_package(LSL QUIET + PATH_SUFFIXES ${_lsl_suffixes} + ) + endif() + + if(LSL_FOUND) + message(STATUS "Found installed liblsl: ${LSL_DIR}") + elseif(LSL_FETCH_IF_MISSING) + # Priority 4: Fetch from GitHub + message(STATUS "liblsl not found locally, fetching ${LSL_FETCH_REF} from GitHub...") + include(FetchContent) + # Disable liblsl extras we don't need + set(LSL_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(LSL_BUILD_TESTING OFF CACHE BOOL "" FORCE) + # EXCLUDE_FROM_ALL prevents liblsl's install rules from running + FetchContent_Declare(liblsl + GIT_REPOSITORY https://github.com/sccn/liblsl.git + GIT_TAG ${LSL_FETCH_REF} + GIT_SHALLOW ON + EXCLUDE_FROM_ALL + ) + FetchContent_MakeAvailable(liblsl) + if(NOT TARGET LSL::lsl) + add_library(LSL::lsl ALIAS lsl) + endif() + set(LSL_FOUND TRUE) + message(STATUS "liblsl fetched and configured") + else() + message(FATAL_ERROR + "liblsl not found. Options:\n" + " 1. Set LSL_SOURCE_DIR to liblsl source directory\n" + " 2. Set LSL_INSTALL_ROOT to installed liblsl location\n" + " 3. Install liblsl system-wide\n" + " 4. Enable LSL_FETCH_IF_MISSING=ON to auto-fetch from GitHub" + ) + endif() +endif() + +# ============================================================================= +# Qt6 +# ============================================================================= +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +find_package(Qt6 REQUIRED COMPONENTS Core Widgets Multimedia) + +# macOS: Work around deprecated AGL framework issue in some Qt6 builds +if(APPLE AND TARGET WrapOpenGL::WrapOpenGL) + get_target_property(_wrap_gl_libs WrapOpenGL::WrapOpenGL INTERFACE_LINK_LIBRARIES) + if(_wrap_gl_libs) + list(FILTER _wrap_gl_libs EXCLUDE REGEX ".*AGL.*") + set_target_properties(WrapOpenGL::WrapOpenGL PROPERTIES INTERFACE_LINK_LIBRARIES "${_wrap_gl_libs}") + endif() +endif() + +# ============================================================================= +# Common Dependencies +# ============================================================================= find_package(Threads REQUIRED) -# Create the target -qt_add_executable(${PROJECT_NAME} MACOSX_BUNDLE MANUAL_FINALIZATION) -set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.labstreaminglayer.AudioCapture") +# ============================================================================= +# RPATH Configuration (must be set before targets are created) +# ============================================================================= +if(APPLE) + set(CMAKE_INSTALL_RPATH "@executable_path/../Frameworks") +elseif(UNIX AND NOT ANDROID) + set(CMAKE_INSTALL_RPATH "$ORIGIN;$ORIGIN/../lib") +endif() +set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) + +# ============================================================================= +# Application Target +# ============================================================================= +qt_add_executable(${PROJECT_NAME} MACOSX_BUNDLE) + target_sources(${PROJECT_NAME} PRIVATE main.cpp mainwindow.cpp @@ -44,108 +167,210 @@ target_sources(${PROJECT_NAME} PRIVATE reader.cpp ) -target_link_libraries(${PROJECT_NAME} - PRIVATE +target_link_libraries(${PROJECT_NAME} PRIVATE Qt::Widgets Qt::Multimedia - Qt::DBus Threads::Threads LSL::lsl ) +# macOS bundle configuration if(APPLE) - # Code signing is not required for debugging, and our deployment script will handle signing for release builds. - set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "NO") - set_target_properties(${PROJECT_NAME} PROPERTIES - XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "NO" - ) -endif(APPLE) - -if(WIN32) - # Copy all required DLLs into the build folder --> useful for debugging from IDE - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $ - COMMAND_EXPAND_LISTS - COMMENT "Copying runtime dependencies for ${PROJECT_NAME}" - ) - get_target_property(_qmake_executable Qt6::qmake IMPORTED_LOCATION) - execute_process( - COMMAND ${_qmake_executable} -query QT_INSTALL_PLUGINS - OUTPUT_VARIABLE QT_PLUGINS_DIR - OUTPUT_STRIP_TRAILING_WHITESPACE + set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.labstreaminglayer.${PROJECT_NAME}") + set(MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}") + set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}") + set_target_properties(${PROJECT_NAME} PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/cmake/MacOSXBundleInfo.plist.in" ) - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory - "${QT_PLUGINS_DIR}/platforms" - "$/platforms") - add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory - "${QT_PLUGINS_DIR}/styles" - "$/styles") -endif(WIN32) +endif() qt_finalize_target(${PROJECT_NAME}) -install( - TARGETS ${PROJECT_NAME} - COMPONENT ${PROJECT_NAME} - BUNDLE DESTINATION . - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} -) +# ============================================================================= +# Installation +# ============================================================================= +include(GNUInstallDirs) +# Platform-specific install directories if(WIN32) - install(FILES $ - DESTINATION ${CMAKE_INSTALL_BINDIR} - COMPONENT ${PROJECT_NAME} - ) -endif() - -if(APPLE) - # Install LSL framework into bundle - get_filename_component(LSL_RESOURCES_DIR ${LSL_PATH} DIRECTORY) - get_filename_component(LSL_FRAMEWORK_DIR ${LSL_RESOURCES_DIR} DIRECTORY) - install(DIRECTORY "${LSL_FRAMEWORK_DIR}" - DESTINATION "${PROJECT_NAME}.app/Contents/Frameworks" - COMPONENT ${PROJECT_NAME}) - - # The following should already be part of the INSTALL_RPATH so we don't need to append it. - # set_property(TARGET ${PROJECT_NAME} APPEND PROPERTY INSTALL_RPATH "@executable_path/../Frameworks") - - # Additionally, the qt_generate_deploy_app_script() will remove /Library/Frameworks from the rpath, - # meaning that we don't have to worry about the LSL framework being found in the system paths. - # i.e., it's OK that the install name for lsl.framework is set to @rpath/lsl.framework/Versions/A/lsl. - # and not to @executable_path/../Frameworks/lsl.framework/Versions/A/lsl. + set(INSTALL_BINDIR ".") + set(INSTALL_LIBDIR ".") +elseif(APPLE) + set(INSTALL_BINDIR ".") + set(INSTALL_LIBDIR ".") +else() + set(INSTALL_BINDIR "${CMAKE_INSTALL_BINDIR}") + set(INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}") endif() -qt_generate_deploy_app_script( - TARGET ${PROJECT_NAME} - OUTPUT_SCRIPT deploy_script_path - NO_TRANSLATIONS - NO_UNSUPPORTED_PLATFORM_ERROR +install(TARGETS ${PROJECT_NAME} + BUNDLE DESTINATION "${INSTALL_BINDIR}" + RUNTIME DESTINATION "${INSTALL_BINDIR}" ) -install(SCRIPT ${deploy_script_path}) + +# ============================================================================= +# Bundle liblsl with the application +# ============================================================================= +# Detect if liblsl is from FetchContent (regular target) or find_package (imported) +set(_lsl_is_fetched FALSE) +if(TARGET lsl) + get_target_property(_lsl_imported lsl IMPORTED) + if(NOT _lsl_imported) + set(_lsl_is_fetched TRUE) + endif() +endif() if(APPLE) - # Add a post-install step to codesign the app bundle, but only if the APPLE_CODE_SIGN_IDENTITY_APP is set. + # macOS: Install framework into app bundle install(CODE " - if(NOT \"$ENV{APPLE_CODE_SIGN_IDENTITY_APP}\" STREQUAL \"\") - message(STATUS \"Codesigning app bundle with identity: $ENV{APPLE_CODE_SIGN_IDENTITY_APP}\") + set(_lsl_binary \"$\") + cmake_path(GET _lsl_binary PARENT_PATH _lsl_fw_dir) # Versions/A + cmake_path(GET _lsl_fw_dir PARENT_PATH _lsl_fw_dir) # Versions + cmake_path(GET _lsl_fw_dir PARENT_PATH _lsl_fw_dir) # lsl.framework + message(STATUS \"Bundling lsl.framework from: \${_lsl_fw_dir}\") + file(COPY \"\${_lsl_fw_dir}\" + DESTINATION \"\${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR}/${PROJECT_NAME}.app/Contents/Frameworks\" + USE_SOURCE_PERMISSIONS + ) + ") +elseif(WIN32) + if(_lsl_is_fetched) + install(TARGETS lsl RUNTIME DESTINATION "${INSTALL_BINDIR}") + else() + install(IMPORTED_RUNTIME_ARTIFACTS LSL::lsl RUNTIME DESTINATION "${INSTALL_BINDIR}") + endif() +else() + # Linux: Use modern CMake install commands that handle symlinks properly + if(_lsl_is_fetched) + install(TARGETS lsl LIBRARY DESTINATION "${INSTALL_LIBDIR}") + else() + install(IMPORTED_RUNTIME_ARTIFACTS LSL::lsl LIBRARY DESTINATION "${INSTALL_LIBDIR}") + endif() +endif() + +# ============================================================================= +# Qt Deployment +# ============================================================================= +get_target_property(QT_QMAKE_EXECUTABLE Qt6::qmake IMPORTED_LOCATION) +get_filename_component(QT_BIN_DIR "${QT_QMAKE_EXECUTABLE}" DIRECTORY) + +if(WIN32) + find_program(WINDEPLOYQT_EXECUTABLE windeployqt HINTS "${QT_BIN_DIR}") + if(WINDEPLOYQT_EXECUTABLE) + install(CODE " + message(STATUS \"Running windeployqt...\") execute_process( - COMMAND codesign -vvv --force --deep --sign \"$ENV{APPLE_CODE_SIGN_IDENTITY_APP}\" --options runtime \"${CMAKE_INSTALL_PREFIX}/${PROJECT_NAME}.app\" - RESULT_VARIABLE result + COMMAND \"${WINDEPLOYQT_EXECUTABLE}\" + --no-translations + --no-system-d3d-compiler + --no-opengl-sw + --no-compiler-runtime + --dir \"\${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR}\" + \"\${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR}/${PROJECT_NAME}.exe\" ) - if(NOT result EQUAL 0) - message(FATAL_ERROR \"codesign failed for ${PROJECT_NAME}.app with exit code ${result}\") + ") + endif() +elseif(APPLE) + find_program(MACDEPLOYQT_EXECUTABLE macdeployqt HINTS "${QT_BIN_DIR}") + if(MACDEPLOYQT_EXECUTABLE) + install(CODE " + message(STATUS \"Running macdeployqt...\") + execute_process( + COMMAND \"${MACDEPLOYQT_EXECUTABLE}\" + \"\${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR}/${PROJECT_NAME}.app\" + -verbose=0 + -always-overwrite + RESULT_VARIABLE _deploy_result + ERROR_QUIET + ) + if(NOT _deploy_result EQUAL 0) + message(WARNING \"macdeployqt returned \${_deploy_result}\") endif() + ") + endif() +endif() + +# ============================================================================= +# macOS Code Signing +# ============================================================================= +if(APPLE) + install(CODE " + set(_app \"\${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR}/${PROJECT_NAME}.app\") + set(_ent \"${CMAKE_CURRENT_SOURCE_DIR}/app.entitlements\") + + message(STATUS \"Signing app bundle...\") + execute_process( + COMMAND codesign --force --deep --sign - --entitlements \"\${_ent}\" \"\${_app}\" + RESULT_VARIABLE _sign_result + ) + + execute_process(COMMAND codesign --verify --verbose \"\${_app}\" RESULT_VARIABLE _verify_result) + if(_verify_result EQUAL 0) + message(STATUS \"App bundle signature verified successfully\") + else() + message(WARNING \"App bundle signature verification failed!\") + endif() + ") +endif() + +# ============================================================================= +# CPack Configuration +# ============================================================================= + +# Detect target architecture +if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)") + set(PACKAGE_ARCH "arm64") +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)") + set(PACKAGE_ARCH "amd64") +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(armv7|arm)") + set(PACKAGE_ARCH "armhf") +elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(i.86|x86)") + set(PACKAGE_ARCH "i386") +else() + set(PACKAGE_ARCH "${CMAKE_SYSTEM_PROCESSOR}") +endif() + +# Detect OS for package naming +if(APPLE) + set(PACKAGE_OS "macOS") +elseif(WIN32) + set(PACKAGE_OS "Win") +else() + find_program(LSB_RELEASE lsb_release) + if(LSB_RELEASE) + execute_process(COMMAND ${LSB_RELEASE} -cs + OUTPUT_VARIABLE _codename + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET) + if(_codename AND NOT _codename STREQUAL "n/a") + set(PACKAGE_OS "${_codename}") + else() + set(PACKAGE_OS "Linux") endif() - " COMPONENT ${PROJECT_NAME}) + else() + set(PACKAGE_OS "Linux") + endif() endif() -#installLSLApp(${PROJECT_NAME}) +set(CPACK_PACKAGE_NAME "${PROJECT_NAME}") +set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") +set(CPACK_PACKAGE_VENDOR "Labstreaminglayer") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PROJECT_DESCRIPTION}") +set(CPACK_PACKAGE_HOMEPAGE_URL "${PROJECT_HOMEPAGE_URL}") +set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-${PACKAGE_OS}_${PACKAGE_ARCH}") +set(CPACK_STRIP_FILES ON) + +if(WIN32) + set(CPACK_GENERATOR ZIP) +elseif(APPLE) + set(CPACK_GENERATOR TGZ) +else() + set(CPACK_GENERATOR DEB TGZ) + set(CPACK_DEBIAN_PACKAGE_MAINTAINER "LabStreamingLayer Developers") + set(CPACK_DEBIAN_PACKAGE_SECTION "science") + set(CPACK_DEBIAN_FILE_NAME "${CPACK_PACKAGE_FILE_NAME}.deb") + set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS OFF) + set(CPACK_DEBIAN_PACKAGE_DEPENDS "libqt6widgets6, libqt6multimedia6") +endif() -# TODO: Generate CPack config file for DMG -#LSLGenerateCPackConfig() +include(CPack) diff --git a/app.entitlements b/app.entitlements new file mode 100644 index 0000000..9a7d01a --- /dev/null +++ b/app.entitlements @@ -0,0 +1,21 @@ + + + + + + com.apple.security.network.client + + + + com.apple.security.network.server + + + + com.apple.security.network.multicast + + + + com.apple.security.device.audio-input + + + diff --git a/cmake/MacOSXBundleInfo.plist b/cmake/MacOSXBundleInfo.plist.in similarity index 100% rename from cmake/MacOSXBundleInfo.plist rename to cmake/MacOSXBundleInfo.plist.in diff --git a/scripts/sign_and_notarize.sh b/scripts/sign_and_notarize.sh new file mode 100755 index 0000000..425e9db --- /dev/null +++ b/scripts/sign_and_notarize.sh @@ -0,0 +1,107 @@ +#!/bin/bash +# ============================================================================= +# Apple Code Signing and Notarization Script +# ============================================================================= +# This script handles identity-based signing and notarization for macOS apps. +# It's designed to be called from CI (GitHub Actions) after the build. +# +# Usage: +# ./scripts/sign_and_notarize.sh [--notarize] +# +# Environment Variables (set in CI): +# APPLE_CODE_SIGN_IDENTITY_APP - Developer ID Application certificate name +# APPLE_NOTARIZE_KEYCHAIN_PROFILE - notarytool credential profile name +# ENTITLEMENTS_FILE - Path to entitlements file (optional) +# +# Examples: +# ./scripts/sign_and_notarize.sh build/install/AudioCapture.app +# ./scripts/sign_and_notarize.sh build/install/AudioCapture.app --notarize +# ============================================================================= + +set -e + +APP_PATH="$1" +DO_NOTARIZE=false + +if [[ "$2" == "--notarize" ]]; then + DO_NOTARIZE=true +fi + +if [[ -z "$APP_PATH" ]]; then + echo "Usage: $0 [--notarize]" + exit 1 +fi + +if [[ ! -e "$APP_PATH" ]]; then + echo "Error: $APP_PATH does not exist" + exit 1 +fi + +SIGN_IDENTITY="${APPLE_CODE_SIGN_IDENTITY_APP:--}" +ENTITLEMENTS_ARG="" + +if [[ -n "${ENTITLEMENTS_FILE}" && -f "${ENTITLEMENTS_FILE}" ]]; then + ENTITLEMENTS_ARG="--entitlements ${ENTITLEMENTS_FILE}" +elif [[ -f "$(dirname "$0")/../app.entitlements" ]]; then + ENTITLEMENTS_ARG="--entitlements $(dirname "$0")/../app.entitlements" +fi + +echo "=== Code Signing ===" +echo "Target: $APP_PATH" +echo "Identity: $SIGN_IDENTITY" +echo "Entitlements: ${ENTITLEMENTS_ARG:-none}" + +if [[ -d "$APP_PATH" ]]; then + codesign --force --deep --sign "$SIGN_IDENTITY" \ + --options runtime \ + $ENTITLEMENTS_ARG \ + "$APP_PATH" +else + codesign --force --sign "$SIGN_IDENTITY" \ + --options runtime \ + $ENTITLEMENTS_ARG \ + "$APP_PATH" +fi + +echo "Verifying signature..." +codesign --verify --verbose "$APP_PATH" + +if [[ "$DO_NOTARIZE" == true ]]; then + if [[ "$SIGN_IDENTITY" == "-" ]]; then + echo "Warning: Cannot notarize with ad-hoc signature. Skipping notarization." + exit 0 + fi + + if [[ -z "$APPLE_NOTARIZE_KEYCHAIN_PROFILE" ]]; then + echo "Error: APPLE_NOTARIZE_KEYCHAIN_PROFILE not set" + exit 1 + fi + + echo "" + echo "=== Notarizing ===" + + BASENAME=$(basename "$APP_PATH") + ZIP_PATH="/tmp/${BASENAME%.*}_notarize.zip" + + echo "Creating zip for submission: $ZIP_PATH" + ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH" + + echo "Submitting to Apple notarization service..." + xcrun notarytool submit "$ZIP_PATH" \ + --keychain-profile "$APPLE_NOTARIZE_KEYCHAIN_PROFILE" \ + --wait + + if [[ -d "$APP_PATH" ]]; then + echo "Stapling notarization ticket..." + xcrun stapler staple "$APP_PATH" + xcrun stapler validate "$APP_PATH" + fi + + rm -f "$ZIP_PATH" + + echo "" + echo "=== Notarization Complete ===" +fi + +echo "" +echo "Done!" From 95607abf30d809cf3fe0f20747cde74a4711d643 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 9 Jan 2026 18:53:00 -0500 Subject: [PATCH 13/19] Windows copy DLLs post-build --- CMakeLists.txt | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index a85c750..d0cfa29 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -186,6 +186,39 @@ endif() qt_finalize_target(${PROJECT_NAME}) +# ============================================================================= +# Windows: Copy DLLs to build directory for debugging +# ============================================================================= +if(WIN32) + get_target_property(_qmake_executable Qt6::qmake IMPORTED_LOCATION) + execute_process( + COMMAND ${_qmake_executable} -query QT_INSTALL_PLUGINS + OUTPUT_VARIABLE QT_PLUGINS_DIR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + # Copy runtime DLLs + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ + COMMAND_EXPAND_LISTS + COMMENT "Copying runtime DLLs for ${PROJECT_NAME}" + ) + + # Copy Qt plugins + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${QT_PLUGINS_DIR}/platforms" + "$/platforms" + ) + add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${QT_PLUGINS_DIR}/styles" + "$/styles" + ) +endif() + # ============================================================================= # Installation # ============================================================================= From fbd414f94d00dd8464eeb8e491b297eff36e602b Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 9 Jan 2026 23:18:25 -0500 Subject: [PATCH 14/19] Update CMakeLists.txt for liblsl search and MinGW dll copy --- CMakeLists.txt | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d0cfa29..3832301 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -58,10 +58,15 @@ else() if(LSL_INSTALL_ROOT) list(APPEND _lsl_hints "${LSL_INSTALL_ROOT}") endif() - # Common development layout hints + # Common development layout hints (including CLion cmake-build-* directories) string(TOLOWER "${CMAKE_BUILD_TYPE}" _build_type_lower) + if(MSVC) + set(_clion_build_dir "cmake-build-${_build_type_lower}-visual-studio") + else() + set(_clion_build_dir "cmake-build-${_build_type_lower}") + endif() foreach(_root IN ITEMS "../liblsl" "../../LSL/liblsl") - foreach(_build IN ITEMS "build" "cmake-build-${_build_type_lower}") + foreach(_build IN ITEMS "build" "${_clion_build_dir}") list(APPEND _lsl_hints "${CMAKE_CURRENT_LIST_DIR}/${_root}/${_build}/install") endforeach() endforeach() @@ -323,6 +328,24 @@ elseif(APPLE) endif() endif() +# ============================================================================= +# MinGW Runtime Deployment +# Copy MinGW runtime DLLs so executables work outside the build environment +# ============================================================================= +if(MINGW) + get_filename_component(MINGW_BIN_DIR "${CMAKE_CXX_COMPILER}" DIRECTORY) + set(MINGW_RUNTIME_DLLS + "${MINGW_BIN_DIR}/libgcc_s_seh-1.dll" + "${MINGW_BIN_DIR}/libstdc++-6.dll" + "${MINGW_BIN_DIR}/libwinpthread-1.dll" + ) + foreach(_dll ${MINGW_RUNTIME_DLLS}) + if(EXISTS "${_dll}") + install(FILES "${_dll}" DESTINATION "${INSTALL_BINDIR}") + endif() + endforeach() +endif() + # ============================================================================= # macOS Code Signing # ============================================================================= From 30c3c078e52c98778396bebcd71b78b8272c34cd Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Fri, 9 Jan 2026 23:23:02 -0500 Subject: [PATCH 15/19] Remove unnecessary qt_finalize_target --- CMakeLists.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3832301..71a3a3b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -189,8 +189,6 @@ if(APPLE) ) endif() -qt_finalize_target(${PROJECT_NAME}) - # ============================================================================= # Windows: Copy DLLs to build directory for debugging # ============================================================================= From 3904ef8a50e2fa502c7375a5a5fa07129e233910 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Sat, 10 Jan 2026 02:51:05 -0500 Subject: [PATCH 16/19] Leverage LSLCMake.cmake --- CMakeLists.txt | 171 +++++++------------------------------------------ 1 file changed, 22 insertions(+), 149 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 71a3a3b..9c0d782 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,7 @@ if(LSL_SOURCE_DIR) add_library(LSL::lsl ALIAS lsl) endif() set(LSL_FOUND TRUE) + include("${LSL_SOURCE_DIR}/cmake/LSLCMake.cmake") else() # Priority 2 & 3: Try to find installed liblsl set(_lsl_hints) @@ -60,6 +61,9 @@ else() endif() # Common development layout hints (including CLion cmake-build-* directories) string(TOLOWER "${CMAKE_BUILD_TYPE}" _build_type_lower) + if(NOT _build_type_lower) + set(_build_type_lower "release") + endif() if(MSVC) set(_clion_build_dir "cmake-build-${_build_type_lower}-visual-studio") else() @@ -94,6 +98,7 @@ else() if(LSL_FOUND) message(STATUS "Found installed liblsl: ${LSL_DIR}") + include("${LSL_DIR}/LSLCMake.cmake") elseif(LSL_FETCH_IF_MISSING) # Priority 4: Fetch from GitHub message(STATUS "liblsl not found locally, fetching ${LSL_FETCH_REF} from GitHub...") @@ -113,6 +118,7 @@ else() add_library(LSL::lsl ALIAS lsl) endif() set(LSL_FOUND TRUE) + include("${liblsl_SOURCE_DIR}/cmake/LSLCMake.cmake") message(STATUS "liblsl fetched and configured") else() message(FATAL_ERROR @@ -151,12 +157,7 @@ find_package(Threads REQUIRED) # ============================================================================= # RPATH Configuration (must be set before targets are created) # ============================================================================= -if(APPLE) - set(CMAKE_INSTALL_RPATH "@executable_path/../Frameworks") -elseif(UNIX AND NOT ANDROID) - set(CMAKE_INSTALL_RPATH "$ORIGIN;$ORIGIN/../lib") -endif() -set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) +LSL_configure_rpath() # ============================================================================= # Application Target @@ -247,171 +248,43 @@ install(TARGETS ${PROJECT_NAME} # ============================================================================= # Bundle liblsl with the application # ============================================================================= -# Detect if liblsl is from FetchContent (regular target) or find_package (imported) -set(_lsl_is_fetched FALSE) -if(TARGET lsl) - get_target_property(_lsl_imported lsl IMPORTED) - if(NOT _lsl_imported) - set(_lsl_is_fetched TRUE) - endif() -endif() - -if(APPLE) - # macOS: Install framework into app bundle - install(CODE " - set(_lsl_binary \"$\") - cmake_path(GET _lsl_binary PARENT_PATH _lsl_fw_dir) # Versions/A - cmake_path(GET _lsl_fw_dir PARENT_PATH _lsl_fw_dir) # Versions - cmake_path(GET _lsl_fw_dir PARENT_PATH _lsl_fw_dir) # lsl.framework - message(STATUS \"Bundling lsl.framework from: \${_lsl_fw_dir}\") - file(COPY \"\${_lsl_fw_dir}\" - DESTINATION \"\${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR}/${PROJECT_NAME}.app/Contents/Frameworks\" - USE_SOURCE_PERMISSIONS - ) - ") -elseif(WIN32) - if(_lsl_is_fetched) - install(TARGETS lsl RUNTIME DESTINATION "${INSTALL_BINDIR}") - else() - install(IMPORTED_RUNTIME_ARTIFACTS LSL::lsl RUNTIME DESTINATION "${INSTALL_BINDIR}") - endif() -else() - # Linux: Use modern CMake install commands that handle symlinks properly - if(_lsl_is_fetched) - install(TARGETS lsl LIBRARY DESTINATION "${INSTALL_LIBDIR}") - else() - install(IMPORTED_RUNTIME_ARTIFACTS LSL::lsl LIBRARY DESTINATION "${INSTALL_LIBDIR}") - endif() -endif() +LSL_install_liblsl( + DESTINATION "${INSTALL_LIBDIR}" + FRAMEWORK_DESTINATION "${INSTALL_BINDIR}/${PROJECT_NAME}.app/Contents/Frameworks" +) # ============================================================================= # Qt Deployment # ============================================================================= -get_target_property(QT_QMAKE_EXECUTABLE Qt6::qmake IMPORTED_LOCATION) -get_filename_component(QT_BIN_DIR "${QT_QMAKE_EXECUTABLE}" DIRECTORY) - -if(WIN32) - find_program(WINDEPLOYQT_EXECUTABLE windeployqt HINTS "${QT_BIN_DIR}") - if(WINDEPLOYQT_EXECUTABLE) - install(CODE " - message(STATUS \"Running windeployqt...\") - execute_process( - COMMAND \"${WINDEPLOYQT_EXECUTABLE}\" - --no-translations - --no-system-d3d-compiler - --no-opengl-sw - --no-compiler-runtime - --dir \"\${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR}\" - \"\${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR}/${PROJECT_NAME}.exe\" - ) - ") - endif() -elseif(APPLE) - find_program(MACDEPLOYQT_EXECUTABLE macdeployqt HINTS "${QT_BIN_DIR}") - if(MACDEPLOYQT_EXECUTABLE) - install(CODE " - message(STATUS \"Running macdeployqt...\") - execute_process( - COMMAND \"${MACDEPLOYQT_EXECUTABLE}\" - \"\${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR}/${PROJECT_NAME}.app\" - -verbose=0 - -always-overwrite - RESULT_VARIABLE _deploy_result - ERROR_QUIET - ) - if(NOT _deploy_result EQUAL 0) - message(WARNING \"macdeployqt returned \${_deploy_result}\") - endif() - ") - endif() -endif() +LSL_deploy_qt(TARGET "${PROJECT_NAME}" DESTINATION "${INSTALL_BINDIR}") # ============================================================================= # MinGW Runtime Deployment -# Copy MinGW runtime DLLs so executables work outside the build environment # ============================================================================= -if(MINGW) - get_filename_component(MINGW_BIN_DIR "${CMAKE_CXX_COMPILER}" DIRECTORY) - set(MINGW_RUNTIME_DLLS - "${MINGW_BIN_DIR}/libgcc_s_seh-1.dll" - "${MINGW_BIN_DIR}/libstdc++-6.dll" - "${MINGW_BIN_DIR}/libwinpthread-1.dll" - ) - foreach(_dll ${MINGW_RUNTIME_DLLS}) - if(EXISTS "${_dll}") - install(FILES "${_dll}" DESTINATION "${INSTALL_BINDIR}") - endif() - endforeach() -endif() +LSL_install_mingw_runtime(DESTINATION "${INSTALL_BINDIR}") # ============================================================================= # macOS Code Signing # ============================================================================= -if(APPLE) - install(CODE " - set(_app \"\${CMAKE_INSTALL_PREFIX}/${INSTALL_BINDIR}/${PROJECT_NAME}.app\") - set(_ent \"${CMAKE_CURRENT_SOURCE_DIR}/app.entitlements\") - - message(STATUS \"Signing app bundle...\") - execute_process( - COMMAND codesign --force --deep --sign - --entitlements \"\${_ent}\" \"\${_app}\" - RESULT_VARIABLE _sign_result - ) - - execute_process(COMMAND codesign --verify --verbose \"\${_app}\" RESULT_VARIABLE _verify_result) - if(_verify_result EQUAL 0) - message(STATUS \"App bundle signature verified successfully\") - else() - message(WARNING \"App bundle signature verification failed!\") - endif() - ") -endif() +LSL_codesign( + TARGET "${PROJECT_NAME}" + DESTINATION "${INSTALL_BINDIR}" + ENTITLEMENTS "${CMAKE_CURRENT_SOURCE_DIR}/app.entitlements" + BUNDLE +) # ============================================================================= # CPack Configuration # ============================================================================= - -# Detect target architecture -if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)") - set(PACKAGE_ARCH "arm64") -elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)") - set(PACKAGE_ARCH "amd64") -elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(armv7|arm)") - set(PACKAGE_ARCH "armhf") -elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(i.86|x86)") - set(PACKAGE_ARCH "i386") -else() - set(PACKAGE_ARCH "${CMAKE_SYSTEM_PROCESSOR}") -endif() - -# Detect OS for package naming -if(APPLE) - set(PACKAGE_OS "macOS") -elseif(WIN32) - set(PACKAGE_OS "Win") -else() - find_program(LSB_RELEASE lsb_release) - if(LSB_RELEASE) - execute_process(COMMAND ${LSB_RELEASE} -cs - OUTPUT_VARIABLE _codename - OUTPUT_STRIP_TRAILING_WHITESPACE - ERROR_QUIET) - if(_codename AND NOT _codename STREQUAL "n/a") - set(PACKAGE_OS "${_codename}") - else() - set(PACKAGE_OS "Linux") - endif() - else() - set(PACKAGE_OS "Linux") - endif() -endif() +LSL_get_target_arch() +LSL_get_os_name() set(CPACK_PACKAGE_NAME "${PROJECT_NAME}") set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") set(CPACK_PACKAGE_VENDOR "Labstreaminglayer") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PROJECT_DESCRIPTION}") set(CPACK_PACKAGE_HOMEPAGE_URL "${PROJECT_HOMEPAGE_URL}") -set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-${PACKAGE_OS}_${PACKAGE_ARCH}") +set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-${LSL_OS}_${LSL_ARCH}") set(CPACK_STRIP_FILES ON) if(WIN32) From 75c86c5f6239e9b9f6b4b99a60c1cbed32652552 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Sat, 10 Jan 2026 18:39:45 -0500 Subject: [PATCH 17/19] Simplify liblsl-finding --- CMakeLists.txt | 132 +++++++++++-------------------------------------- 1 file changed, 29 insertions(+), 103 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9c0d782..c2c63f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,112 +23,38 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # ============================================================================= -# LSL Discovery Options -# ============================================================================= -# Priority 1: Build from local source (for parallel liblsl development) -set(LSL_SOURCE_DIR "" CACHE PATH "Path to liblsl source directory") - -# Priority 2: Use explicit installation path -set(LSL_INSTALL_ROOT "" CACHE PATH "Path to installed liblsl") - -# Priority 3: System installation (searched automatically) - -# Priority 4: Fetch from GitHub if not found -option(LSL_FETCH_IF_MISSING "Fetch liblsl from GitHub if not found locally" ON) -# TODO: Change back to version tag (e.g., "v1.16.2") once apple_framework branch is merged -set(LSL_FETCH_REF "cboulay/apple_framework" CACHE STRING "liblsl git ref to fetch (tag, branch, or commit)") - -# ============================================================================= -# Find/Fetch liblsl -# ============================================================================= -if(LSL_SOURCE_DIR) - # Priority 1: Build from local source (parallel development) - if(NOT EXISTS "${LSL_SOURCE_DIR}/CMakeLists.txt") - message(FATAL_ERROR "LSL_SOURCE_DIR set to '${LSL_SOURCE_DIR}' but no CMakeLists.txt found there") - endif() - message(STATUS "Using local liblsl source: ${LSL_SOURCE_DIR}") - add_subdirectory("${LSL_SOURCE_DIR}" liblsl_bin EXCLUDE_FROM_ALL) - if(NOT TARGET LSL::lsl) - add_library(LSL::lsl ALIAS lsl) - endif() - set(LSL_FOUND TRUE) - include("${LSL_SOURCE_DIR}/cmake/LSLCMake.cmake") -else() - # Priority 2 & 3: Try to find installed liblsl - set(_lsl_hints) - if(LSL_INSTALL_ROOT) - list(APPEND _lsl_hints "${LSL_INSTALL_ROOT}") - endif() - # Common development layout hints (including CLion cmake-build-* directories) - string(TOLOWER "${CMAKE_BUILD_TYPE}" _build_type_lower) - if(NOT _build_type_lower) - set(_build_type_lower "release") - endif() - if(MSVC) - set(_clion_build_dir "cmake-build-${_build_type_lower}-visual-studio") - else() - set(_clion_build_dir "cmake-build-${_build_type_lower}") - endif() - foreach(_root IN ITEMS "../liblsl" "../../LSL/liblsl") - foreach(_build IN ITEMS "build" "${_clion_build_dir}") - list(APPEND _lsl_hints "${CMAKE_CURRENT_LIST_DIR}/${_root}/${_build}/install") - endforeach() - endforeach() - - set(_lsl_suffixes - share/LSL - lib/cmake/LSL - cmake - Frameworks/lsl.framework/Resources/CMake # macOS framework layout +# liblsl Dependency +# ============================================================================= +# By default, liblsl is fetched automatically from GitHub. +# To use a pre-installed liblsl, set LSL_INSTALL_ROOT. +set(LSL_INSTALL_ROOT "" CACHE PATH "Path to installed liblsl (optional)") +set(LSL_FETCH_REF "v1.17.0" CACHE STRING "liblsl version to fetch from GitHub") + +if(LSL_INSTALL_ROOT) + # Use pre-installed liblsl + find_package(LSL REQUIRED + HINTS "${LSL_INSTALL_ROOT}" + PATH_SUFFIXES share/LSL lib/cmake/LSL Frameworks/lsl.framework/Resources/CMake ) - - # First try: Search only in hints (prefer local builds over system) - find_package(LSL QUIET - HINTS ${_lsl_hints} - PATH_SUFFIXES ${_lsl_suffixes} - NO_DEFAULT_PATH + message(STATUS "Using installed liblsl: ${LSL_DIR}") + include("${LSL_DIR}/LSLCMake.cmake") +else() + # Fetch liblsl from GitHub + message(STATUS "Fetching liblsl ${LSL_FETCH_REF} from GitHub...") + include(FetchContent) + set(LSL_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) + set(LSL_BUILD_TESTING OFF CACHE BOOL "" FORCE) + FetchContent_Declare(liblsl + GIT_REPOSITORY https://github.com/sccn/liblsl.git + GIT_TAG ${LSL_FETCH_REF} + GIT_SHALLOW ON + EXCLUDE_FROM_ALL ) - - # Second try: Search system paths if not found in hints - if(NOT LSL_FOUND) - find_package(LSL QUIET - PATH_SUFFIXES ${_lsl_suffixes} - ) - endif() - - if(LSL_FOUND) - message(STATUS "Found installed liblsl: ${LSL_DIR}") - include("${LSL_DIR}/LSLCMake.cmake") - elseif(LSL_FETCH_IF_MISSING) - # Priority 4: Fetch from GitHub - message(STATUS "liblsl not found locally, fetching ${LSL_FETCH_REF} from GitHub...") - include(FetchContent) - # Disable liblsl extras we don't need - set(LSL_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) - set(LSL_BUILD_TESTING OFF CACHE BOOL "" FORCE) - # EXCLUDE_FROM_ALL prevents liblsl's install rules from running - FetchContent_Declare(liblsl - GIT_REPOSITORY https://github.com/sccn/liblsl.git - GIT_TAG ${LSL_FETCH_REF} - GIT_SHALLOW ON - EXCLUDE_FROM_ALL - ) - FetchContent_MakeAvailable(liblsl) - if(NOT TARGET LSL::lsl) - add_library(LSL::lsl ALIAS lsl) - endif() - set(LSL_FOUND TRUE) - include("${liblsl_SOURCE_DIR}/cmake/LSLCMake.cmake") - message(STATUS "liblsl fetched and configured") - else() - message(FATAL_ERROR - "liblsl not found. Options:\n" - " 1. Set LSL_SOURCE_DIR to liblsl source directory\n" - " 2. Set LSL_INSTALL_ROOT to installed liblsl location\n" - " 3. Install liblsl system-wide\n" - " 4. Enable LSL_FETCH_IF_MISSING=ON to auto-fetch from GitHub" - ) + FetchContent_MakeAvailable(liblsl) + if(NOT TARGET LSL::lsl) + add_library(LSL::lsl ALIAS lsl) endif() + include("${liblsl_SOURCE_DIR}/cmake/LSLCMake.cmake") endif() # ============================================================================= From 7d63568c132fe70ff6703b7dcf9a20c422d2d75e Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 12 Jan 2026 12:16:27 -0500 Subject: [PATCH 18/19] bump liblsl version --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c2c63f3..ed3af33 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,7 +28,7 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # By default, liblsl is fetched automatically from GitHub. # To use a pre-installed liblsl, set LSL_INSTALL_ROOT. set(LSL_INSTALL_ROOT "" CACHE PATH "Path to installed liblsl (optional)") -set(LSL_FETCH_REF "v1.17.0" CACHE STRING "liblsl version to fetch from GitHub") +set(LSL_FETCH_REF "v1.17.4" CACHE STRING "liblsl version to fetch from GitHub") if(LSL_INSTALL_ROOT) # Use pre-installed liblsl From 78e37137deae62b8271ccd8067a511c93f2f4b93 Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Mon, 12 Jan 2026 16:22:07 -0500 Subject: [PATCH 19/19] Don't upload non-signed macos builds to release --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index df941eb..4beaeac 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -253,5 +253,5 @@ jobs: with: files: | artifacts/**/*.zip - artifacts/**/*.tar.gz + artifacts/package-ubuntu-*/*.tar.gz artifacts/**/*.deb