commit 75ca319211e73a70656a757d8508fd5f7ff58bc8
parent 684637631358bf4dcdad8abf61d97f6f170b2037
Author: Alexandre BIQUE <bique.alexandre@gmail.com>
Date: Tue, 18 May 2021 22:49:39 +0200
Add minimal host
Diffstat:
39 files changed, 3217 insertions(+), 2 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
@@ -1,6 +1,10 @@
cmake_minimum_required(VERSION 3.20)
project(CLAP C CXX)
+set(ENABLE_CLAP_HOST FALSE CACHE BOOL "Enables the example host")
+
include_directories(include)
add_executable(clap-compile-test-c src/main.c)
-add_executable(clap-compile-test-cpp src/main.cc)
-\ No newline at end of file
+add_executable(clap-compile-test-cpp src/main.cc)
+
+add_subdirectory(examples)
+\ No newline at end of file
diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt
@@ -0,0 +1,3 @@
+if (ENABLE_CLAP_HOST)
+ add_subdirectory(host)
+endif()
+\ No newline at end of file
diff --git a/examples/host/.clang-format b/examples/host/.clang-format
@@ -0,0 +1,137 @@
+---
+Language: Cpp
+# BasedOnStyle: LLVM
+AccessModifierOffset: -3
+AlignAfterOpenBracket: Align
+AlignConsecutiveMacros: false
+AlignConsecutiveAssignments: false
+AlignConsecutiveDeclarations: true
+AlignEscapedNewlines: Right
+AlignOperands: true
+AlignTrailingComments: true
+AllowAllArgumentsOnNextLine: true
+AllowAllConstructorInitializersOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: All
+AllowShortLambdasOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: Yes
+BinPackArguments: false
+BinPackParameters: false
+BraceWrapping:
+ AfterCaseLabel: false
+ AfterClass: true
+ AfterControlStatement: true
+ AfterEnum: true
+ AfterFunction: true
+ AfterNamespace: true
+ AfterObjCDeclaration: true
+ AfterStruct: true
+ AfterUnion: true
+ AfterExternBlock: true
+ BeforeCatch: true
+ BeforeElse: true
+ IndentBraces: false
+ SplitEmptyFunction: false
+ SplitEmptyRecord: false
+ SplitEmptyNamespace: false
+BreakBeforeBinaryOperators: None
+BreakBeforeBraces: Attach
+BreakBeforeInheritanceComma: false
+BreakInheritanceList: BeforeColon
+BreakBeforeTernaryOperators: true
+BreakConstructorInitializersBeforeComma: false
+BreakConstructorInitializers: BeforeColon
+BreakAfterJavaFieldAnnotations: false
+BreakStringLiterals: true
+ColumnLimit: 100
+CommentPragmas: '^ IWYU pragma:'
+CompactNamespaces: true
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+ConstructorInitializerIndentWidth: 3
+ContinuationIndentWidth: 3
+Cpp11BracedListStyle: true
+DeriveLineEnding: false
+DerivePointerAlignment: false
+DisableFormat: false
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: true
+ForEachMacros:
+ - foreach
+ - Q_FOREACH
+ - BOOST_FOREACH
+IncludeBlocks: Preserve
+IncludeCategories:
+ - Regex: '^"(llvm|llvm-c|clang|clang-c)/'
+ Priority: 2
+ SortPriority: 0
+ - Regex: '^(<|"(gtest|gmock|isl|json)/)'
+ Priority: 3
+ SortPriority: 0
+ - Regex: '.*'
+ Priority: 1
+ SortPriority: 0
+IncludeIsMainRegex: '(Test)?$'
+IncludeIsMainSourceRegex: ''
+IndentCaseLabels: false
+IndentGotoLabels: true
+IndentPPDirectives: AfterHash
+IndentWidth: 3
+IndentWrappedFunctionNames: false
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: true
+MacroBlockBegin: ''
+MacroBlockEnd: ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: All
+ObjCBinPackProtocolList: Auto
+ObjCBlockIndentWidth: 3
+ObjCSpaceAfterProperty: false
+ObjCSpaceBeforeProtocolList: true
+PenaltyBreakAssignment: 2
+PenaltyBreakBeforeFirstCallParameter: 19
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakString: 1000
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 1000000
+PenaltyReturnTypeOnItsOwnLine: 60
+PointerAlignment: Right
+ReflowComments: true
+SortIncludes: true
+SortUsingDeclarations: true
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: true
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeParens: ControlStatements
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyBlock: false
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles: false
+SpacesInConditionalStatement: false
+SpacesInContainerLiterals: true
+SpacesInCStyleCastParentheses: false
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+SpaceBeforeSquareBrackets: false
+Standard: Latest
+StatementMacros:
+ - Q_UNUSED
+ - QT_REQUIRE_VERSION
+TabWidth: 8
+UseCRLF: false
+UseTab: Never
+...
+
diff --git a/examples/host/CMakeLists.txt b/examples/host/CMakeLists.txt
@@ -0,0 +1,72 @@
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+set(CMAKE_AUTOMOC ON)
+
+find_package(Qt6Core REQUIRED)
+find_package(Qt6Widgets REQUIRED)
+#find_package(portaudio REQUIRED)
+#find_package(portmidi REQUIRED)
+
+include_directories(../libs)
+
+add_executable(Host
+ application.cc
+ application.hh
+ audio-settings.cc
+ audio-settings.hh
+ audio-settings-widget.cc
+ audio-settings-widget.hh
+
+ plugin-param.cc
+ plugin-param.hh
+ param-queue.cc
+ param-queue.hh
+ plugin-host.cc
+ plugin-host.hh
+
+ CMakeLists.txt
+ device-reference.cc
+ device-reference.hh
+ engine.cc
+ engine.hh
+ main.cc
+ main-window.cc
+ main-window.hh
+ midi-settings.cc
+ midi-settings.hh
+ midi-settings-widget.cc
+ midi-settings-widget.hh
+ plugin-info.cc
+ plugin-info.hh
+ plugin-parameters-widget.cc
+ plugin-parameters-widget.hh
+ settings.cc
+ settings-dialog.cc
+ settings-dialog.hh
+ settings.hh
+ settings-widget.cc
+ settings-widget.hh
+ )
+
+set_target_properties(Host PROPERTIES CXX_STANDARD 17)
+target_compile_options(Host PRIVATE -fsanitize=address)
+target_link_options(Host PRIVATE -fsanitize=address)
+
+target_link_libraries(Host portmidi portaudio)
+target_link_libraries(Host Qt6::Widgets Qt6::Core)
+
+if (LINUX)
+ target_link_libraries(Host dl pthread)
+endif()
+
+if (APPLE)
+ set_target_properties(Host PROPERTIES OSX_ARCHITECTURES x86_64)
+
+ find_library(CORE_FOUNDATION CoreFoundation)
+ find_library(CORE_AUDIO CoreAudio)
+ find_library(CORE_AUDIO CoreServices)
+ find_library(CORE_MIDI CoreMIDI)
+ find_library(AUDIO_UNIT AudioUnit)
+ find_library(AUDIO_TOOLBOX AudioToolbox)
+ find_library(CARBON Carbon)
+ target_link_libraries(Host ${CARBON} ${AUDIO_UNIT} ${AUDIO_TOOLBOX} ${CORE_MIDI} ${CORE_AUDIO} ${CORE_SERVICES} ${CORE_FOUNDATION})
+endif()
diff --git a/examples/host/application.cc b/examples/host/application.cc
@@ -0,0 +1,101 @@
+#include <cassert>
+
+#ifdef Q_UNIX
+# include <unistd.h>
+#endif
+
+#include <QApplication>
+#include <QCommandLineParser>
+#include <QSettings>
+
+#include "application.hh"
+#include "main-window.hh"
+#include "settings.hh"
+
+Application *Application::instance_ = nullptr;
+
+Q_DECLARE_METATYPE(int32_t)
+Q_DECLARE_METATYPE(uint32_t)
+
+Application::Application(int argc, char **argv)
+ : QApplication(argc, argv), settings_(new Settings) {
+ assert(!instance_);
+ instance_ = this;
+
+ QApplication::setOrganizationDomain("u-he.com");
+ QApplication::setOrganizationName("u-he");
+ QApplication::setApplicationName("uhost");
+ QApplication::setApplicationVersion("1.0");
+
+ parseCommandLine();
+
+ loadSettings();
+
+ engine_ = new Engine(*this);
+
+ mainWindow_ = new MainWindow(*this);
+ mainWindow_->show();
+
+ engine_->setParentWindow(mainWindow_->getEmbedWindowId());
+ QObject::connect(engine_,
+ SIGNAL(resizePluginView(int, int)),
+ mainWindow_,
+ SLOT(resizePluginView(int, int)),
+ Qt::QueuedConnection);
+
+ if (engine_->loadPlugin(pluginPath_, pluginIndex_))
+ engine_->start();
+}
+
+Application::~Application() {
+ saveSettings();
+
+ delete mainWindow_;
+ mainWindow_ = nullptr;
+
+ delete engine_;
+ engine_ = nullptr;
+
+ delete settings_;
+ settings_ = nullptr;
+}
+
+void Application::parseCommandLine() {
+ QCommandLineParser parser;
+
+ QCommandLineOption pluginOpt(QStringList() << "p"
+ << "plugin",
+ tr("path to the plugin"),
+ tr("path"));
+ QCommandLineOption pluginIndexOpt(QStringList() << "i"
+ << "plugin-index",
+ tr("index of the plugin to create"),
+ tr("plugin-index"),
+ "0");
+
+ parser.setApplicationDescription("u-he standalone host");
+ parser.addHelpOption();
+ parser.addVersionOption();
+ parser.addOption(pluginOpt);
+ parser.addOption(pluginIndexOpt);
+
+ parser.process(*this);
+
+ pluginPath_ = parser.value(pluginOpt);
+ pluginIndex_ = parser.value(pluginIndexOpt).toInt();
+}
+
+void Application::loadSettings() {
+ QSettings s;
+ settings_->load(s);
+}
+
+void Application::saveSettings() const {
+ QSettings s;
+ settings_->save(s);
+}
+
+void Application::restartEngine() {
+ engine_->stop();
+ engine_->start();
+}
diff --git a/examples/host/application.hh b/examples/host/application.hh
@@ -0,0 +1,46 @@
+#pragma once
+
+#include <QApplication>
+
+#include "engine.hh"
+
+class MainWindow;
+class Settings;
+class Engine;
+class SpectrumAnalyzer;
+class OscilloscopeAnalyzer;
+class AudioRecorder;
+
+class Application : public QApplication {
+ Q_OBJECT
+
+public:
+ Application(int argc, char **argv);
+ ~Application();
+
+ Settings &settings() { return *settings_; }
+
+ void parseCommandLine();
+
+ void loadSettings();
+ void saveSettings() const;
+
+ MainWindow *mainWindow() const { return mainWindow_; }
+
+ static Application &instance() { return *instance_; }
+
+ Engine *engine() { return engine_; }
+
+public slots:
+ void restartEngine();
+
+private:
+ static Application *instance_;
+
+ Settings * settings_ = nullptr;
+ MainWindow *mainWindow_ = nullptr;
+ Engine * engine_ = nullptr;
+
+ QString pluginPath_;
+ int pluginIndex_ = 0;
+};
diff --git a/examples/host/audio-settings-widget.cc b/examples/host/audio-settings-widget.cc
@@ -0,0 +1,120 @@
+#include <iostream>
+
+#include <QComboBox>
+#include <QGridLayout>
+#include <QGroupBox>
+#include <QLabel>
+#include <QVBoxLayout>
+
+#include <portaudio.h>
+
+#include "audio-settings-widget.hh"
+#include "audio-settings.hh"
+
+static const std::vector<int> SAMPLE_RATES = {
+ 44100,
+ 48000,
+ 88200,
+ 96000,
+ 176400,
+ 192000,
+};
+
+static const std::vector<int> BUFFER_SIZES = {32, 48, 64, 96, 128, 192, 256, 384, 512};
+
+AudioSettingsWidget::AudioSettingsWidget(AudioSettings &audioSettings)
+ : audioSettings_(audioSettings) {
+ /* devices */
+ auto deviceComboBox = new QComboBox(this);
+ auto deviceCount = Pa_GetDeviceCount();
+ bool deviceFound = false;
+
+ for (int i = 0; i < deviceCount; ++i) {
+ auto deviceInfo = Pa_GetDeviceInfo(i);
+ deviceComboBox->addItem(deviceInfo->name);
+
+ if (!deviceFound && audioSettings_.deviceReference().index_ == i &&
+ audioSettings_.deviceReference().name_ == deviceInfo->name) {
+ deviceComboBox->setCurrentIndex(i);
+ deviceFound = true;
+ selectedDeviceChanged(i);
+ }
+ }
+
+ // try to find the device just by its name.
+ for (int i = 0; !deviceFound && i < deviceCount; ++i) {
+ auto deviceInfo = Pa_GetDeviceInfo(i);
+ if (audioSettings_.deviceReference().name_ == deviceInfo->name) {
+ deviceComboBox->setCurrentIndex(i);
+ deviceFound = true;
+ selectedDeviceChanged(i);
+ }
+ }
+
+ connect(
+ deviceComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(selectedDeviceChanged(int)));
+
+ /* sample rate */
+ sampleRateWidget_ = new QComboBox(this);
+ for (size_t i = 0; i < SAMPLE_RATES.size(); ++i) {
+ int sr = SAMPLE_RATES[i];
+ sampleRateWidget_->addItem(QString::number(sr));
+ if (sr == audioSettings_.sampleRate()) {
+ sampleRateWidget_->setCurrentIndex(i);
+ selectedSampleRateChanged(i);
+ }
+ }
+ connect(sampleRateWidget_,
+ SIGNAL(currentIndexChanged(int)),
+ this,
+ SLOT(selectedSampleRateChanged(int)));
+
+ /* buffer size */
+ bufferSizeWidget_ = new QComboBox(this);
+ for (size_t i = 0; i < BUFFER_SIZES.size(); ++i) {
+ int bs = BUFFER_SIZES[i];
+ bufferSizeWidget_->addItem(QString::number(bs));
+ if (bs == audioSettings_.bufferSize()) {
+ bufferSizeWidget_->setCurrentIndex(i);
+ selectedBufferSizeChanged(i);
+ }
+ }
+ connect(bufferSizeWidget_,
+ SIGNAL(currentIndexChanged(int)),
+ this,
+ SLOT(selectedBufferSizeChanged(int)));
+
+ auto layout = new QGridLayout(this);
+ layout->addWidget(new QLabel(tr("Device")), 0, 0);
+ layout->addWidget(new QLabel(tr("Sample rate")), 1, 0);
+ layout->addWidget(new QLabel(tr("Buffer size")), 2, 0);
+
+ layout->addWidget(deviceComboBox, 0, 1);
+ layout->addWidget(sampleRateWidget_, 1, 1);
+ layout->addWidget(bufferSizeWidget_, 2, 1);
+
+ QGroupBox *groupBox = new QGroupBox(this);
+ groupBox->setLayout(layout);
+ groupBox->setTitle(tr("Audio"));
+
+ QLayout *groupLayout = new QVBoxLayout();
+ groupLayout->addWidget(groupBox);
+ setLayout(groupLayout);
+}
+
+void AudioSettingsWidget::selectedDeviceChanged(int index) {
+ auto deviceInfo = Pa_GetDeviceInfo(index);
+
+ DeviceReference ref;
+ ref.index_ = index;
+ ref.name_ = deviceInfo->name;
+ audioSettings_.setDeviceReference(ref);
+}
+
+void AudioSettingsWidget::selectedSampleRateChanged(int index) {
+ audioSettings_.setSampleRate(sampleRateWidget_->itemText(index).toInt());
+}
+
+void AudioSettingsWidget::selectedBufferSizeChanged(int index) {
+ audioSettings_.setBufferSize(bufferSizeWidget_->itemText(index).toInt());
+}
diff --git a/examples/host/audio-settings-widget.hh b/examples/host/audio-settings-widget.hh
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <QWidget>
+
+class AudioSettings;
+class QComboBox;
+
+class AudioSettingsWidget : public QWidget {
+ Q_OBJECT
+public:
+ explicit AudioSettingsWidget(AudioSettings &audioSettings);
+
+signals:
+
+public slots:
+ void selectedDeviceChanged(int index);
+ void selectedSampleRateChanged(int index);
+ void selectedBufferSizeChanged(int index);
+
+private:
+ AudioSettings &audioSettings_;
+ QComboBox * sampleRateWidget_;
+ QComboBox * bufferSizeWidget_;
+};
diff --git a/examples/host/audio-settings.cc b/examples/host/audio-settings.cc
@@ -0,0 +1,24 @@
+#include <QSettings>
+
+#include "audio-settings.hh"
+
+static const char SAMPLE_RATE_KEY[] = "Audio/SampleRate";
+static const char BUFFER_SIZE_KEY[] = "Audio/BufferSize";
+static const char DEVICE_NAME_KEY[] = "Audio/DeviceName";
+static const char DEVICE_INDEX_KEY[] = "Audio/DeviceIndex";
+
+AudioSettings::AudioSettings() {}
+
+void AudioSettings::load(QSettings &settings) {
+ deviceReference_.name_ = settings.value(DEVICE_NAME_KEY).toString();
+ deviceReference_.index_ = settings.value(DEVICE_INDEX_KEY).toInt();
+ sampleRate_ = settings.value(SAMPLE_RATE_KEY, 44100).toInt();
+ bufferSize_ = settings.value(BUFFER_SIZE_KEY, 256).toInt();
+}
+
+void AudioSettings::save(QSettings &settings) const {
+ settings.setValue(SAMPLE_RATE_KEY, sampleRate_);
+ settings.setValue(BUFFER_SIZE_KEY, bufferSize_);
+ settings.setValue(DEVICE_NAME_KEY, deviceReference_.name_);
+ settings.setValue(DEVICE_INDEX_KEY, deviceReference_.index_);
+}
diff --git a/examples/host/audio-settings.hh b/examples/host/audio-settings.hh
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "device-reference.hh"
+
+class QSettings;
+
+class AudioSettings {
+public:
+ AudioSettings();
+
+ void load(QSettings &settings);
+ void save(QSettings &settings) const;
+
+ int sampleRate() const { return sampleRate_; }
+ void setSampleRate(int sampleRate) { sampleRate_ = sampleRate; }
+
+ void setDeviceReference(DeviceReference dr) { deviceReference_ = dr; }
+ const DeviceReference &deviceReference() const { return deviceReference_; }
+
+ int bufferSize() const { return bufferSize_; }
+ void setBufferSize(int bufferSize) { bufferSize_ = bufferSize; }
+
+private:
+ DeviceReference deviceReference_;
+ int sampleRate_ = 44100;
+ int bufferSize_ = 128;
+};
diff --git a/examples/host/device-reference.cc b/examples/host/device-reference.cc
@@ -0,0 +1 @@
+#include "device-reference.hh"
diff --git a/examples/host/device-reference.hh b/examples/host/device-reference.hh
@@ -0,0 +1,8 @@
+#pragma once
+
+#include <QString>
+
+struct DeviceReference {
+ QString name_ = "(noname)";
+ int index_ = 0;
+};
diff --git a/examples/host/engine.cc b/examples/host/engine.cc
@@ -0,0 +1,248 @@
+#include <cassert>
+#include <cstdlib>
+#include <iostream>
+#include <thread>
+
+#include <QApplication>
+#include <QDebug>
+#include <QFile>
+#include <QThread>
+#include <QtGlobal>
+
+#include "application.hh"
+#include "engine.hh"
+#include "main-window.hh"
+#include "plugin-host.hh"
+#include "settings.hh"
+
+enum MidiStatus {
+ MIDI_STATUS_NOTE_OFF = 0x8,
+ MIDI_STATUS_NOTE_ON = 0x9,
+ MIDI_STATUS_NOTE_AT = 0xA, // after touch
+ MIDI_STATUS_CC = 0xB, // control change
+ MIDI_STATUS_PGM_CHANGE = 0xC,
+ MIDI_STATUS_CHANNEL_AT = 0xD, // after touch
+ MIDI_STATUS_PITCH_BEND = 0xE,
+};
+
+Engine::Engine(Application &application)
+ : QObject(&application), application_(application), settings_(application.settings()),
+ idleTimer_(this) {
+ pluginHost_.reset(new PluginHost(*this));
+
+ connect(&idleTimer_, &QTimer::timeout, this, QOverload<>::of(&Engine::callPluginIdle));
+ idleTimer_.start(1000 / 30);
+}
+
+Engine::~Engine() {
+ std::clog << " ####### STOPING ENGINE #########" << std::endl;
+ stop();
+ unloadPlugin();
+ std::clog << " ####### ENGINE STOPPED #########" << std::endl;
+}
+
+void Engine::start() {
+ assert(!audio_);
+ assert(state_ == kStateStopped);
+
+ auto & as = settings_.audioSettings();
+ const int bufferSize = 4 * 2 * as.bufferSize();
+
+ inputs_[0] = (float *)calloc(1, bufferSize);
+ inputs_[1] = (float *)calloc(1, bufferSize);
+ outputs_[0] = (float *)calloc(1, bufferSize);
+ outputs_[1] = (float *)calloc(1, bufferSize);
+
+ pluginHost_->setPorts(2, inputs_, 2, outputs_);
+
+ /* midi */
+ PmError midi_err = Pm_OpenInput(
+ &midi_, settings_.midiSettings().deviceReference().index_, nullptr, 512, nullptr, nullptr);
+ if (midi_err != pmNoError) {
+ midi_ = nullptr;
+ }
+
+ pluginHost_->activate(as.sampleRate());
+
+ /* audio */
+ auto deviceInfo = Pa_GetDeviceInfo(as.deviceReference().index_);
+
+ PaStreamParameters params;
+ params.channelCount = 2;
+ params.device = as.deviceReference().index_;
+ params.hostApiSpecificStreamInfo = nullptr;
+ params.sampleFormat = paFloat32;
+ params.suggestedLatency = 0;
+
+ state_ = kStateRunning;
+ nframes_ = as.bufferSize();
+ PaError err = Pa_OpenStream(&audio_,
+ deviceInfo->maxInputChannels >= 2 ? ¶ms : nullptr,
+ ¶ms,
+ as.sampleRate(),
+ as.bufferSize(),
+ paClipOff | paDitherOff,
+ &Engine::audioCallback,
+ this);
+ if (err != paNoError) {
+ qWarning() << tr("Failed to initialize PortAudio: ") << Pa_GetErrorText(err);
+ stop();
+ return;
+ }
+
+ err = Pa_StartStream(audio_);
+}
+
+void Engine::stop() {
+ if (state_ == kStateRunning)
+ state_ = kStateStopping;
+
+ if (audio_) {
+ Pa_StopStream(audio_);
+ Pa_CloseStream(audio_);
+ audio_ = nullptr;
+ }
+
+ if (midi_) {
+ Pm_Close(midi_);
+ midi_ = nullptr;
+ }
+}
+
+int Engine::audioCallback(const void * input,
+ void * output,
+ unsigned long frameCount,
+ const PaStreamCallbackTimeInfo * /*timeInfo*/,
+ PaStreamCallbackFlags /*statusFlags*/,
+ void *userData) {
+ Engine *const thiz = (Engine *)userData;
+ const float *const in = (const float *)input;
+ float *const out = (float *)output;
+
+ assert(thiz->inputs_[0] != nullptr);
+ assert(thiz->inputs_[1] != nullptr);
+ assert(thiz->outputs_[0] != nullptr);
+ assert(thiz->outputs_[1] != nullptr);
+ assert(frameCount == thiz->nframes_);
+
+ // copy input
+ if (in) {
+ for (int i = 0; i < thiz->nframes_; ++i) {
+ thiz->inputs_[0][i] = in[2 * i];
+ thiz->inputs_[1][i] = in[2 * i + 1];
+ }
+ }
+
+ thiz->pluginHost_->processInit(frameCount);
+
+ MidiSettings &ms = thiz->settings_.midiSettings();
+
+ if (thiz->midi_) {
+ PmEvent evBuffer[512];
+ int numRead = Pm_Read(thiz->midi_, evBuffer, sizeof(evBuffer) / sizeof(evBuffer[0]));
+
+ const PtTimestamp currentTime = Pt_Time();
+
+ PmEvent *ev = evBuffer;
+ for (int i = 0; i < numRead; ++i) {
+ uint8_t eventType = Pm_MessageStatus(ev->message) >> 4;
+ uint8_t channel = Pm_MessageStatus(ev->message) & 0xf;
+ uint8_t data1 = Pm_MessageData1(ev->message);
+ uint8_t data2 = Pm_MessageData2(ev->message);
+
+ int32_t deltaMs = currentTime - ev->timestamp;
+ int32_t deltaSample = (deltaMs * thiz->sampleRate_) / 1000;
+
+ if (deltaSample >= thiz->nframes_)
+ deltaSample = thiz->nframes_ - 1;
+
+ int32_t sampleOffset = thiz->nframes_ - deltaSample;
+
+ switch (eventType) {
+ case MIDI_STATUS_NOTE_ON:
+ thiz->pluginHost_->processNoteOn(sampleOffset, channel, data1, data2);
+ ++ev;
+ break;
+
+ case MIDI_STATUS_NOTE_OFF:
+ thiz->pluginHost_->processNoteOff(sampleOffset, channel, data1, data2);
+ ++ev;
+ break;
+
+ case MIDI_STATUS_CC:
+ thiz->pluginHost_->processCC(sampleOffset, channel, data1, data2);
+ ++ev;
+ break;
+
+ case MIDI_STATUS_NOTE_AT:
+ std::cerr << "Note AT key: " << (int)data1 << ", pres: " << (int)data2 << std::endl;
+ thiz->pluginHost_->processNoteAt(sampleOffset, channel, data1, data2);
+ ++ev;
+ break;
+
+ case MIDI_STATUS_CHANNEL_AT:
+ ++ev;
+ std::cerr << "Channel after touch" << std::endl;
+ break;
+
+ case MIDI_STATUS_PITCH_BEND:
+ thiz->pluginHost_->processPitchBend(sampleOffset, channel, (data2 << 7) | data1);
+ ++ev;
+ break;
+
+ default:
+ std::cerr << "unknown event type: " << (int)eventType << std::endl;
+ ++ev;
+ break;
+ }
+ }
+ }
+
+ thiz->pluginHost_->process();
+
+ // copy output
+ for (int i = 0; i < thiz->nframes_; ++i) {
+ out[2 * i] = thiz->outputs_[0][i];
+ out[2 * i + 1] = thiz->outputs_[1][i];
+ }
+
+ thiz->steadyTime_ += frameCount;
+
+ switch (thiz->state_) {
+ case kStateRunning:
+ return paContinue;
+ case kStateStopping:
+ thiz->state_ = kStateStopped;
+ return paComplete;
+ default:
+ assert(false && "unreachable");
+ return paAbort;
+ }
+}
+
+bool Engine::loadPlugin(const QString &path, int plugin_index) {
+ if (!pluginHost_->load(path, plugin_index))
+ return false;
+
+ pluginHost_->setParentWindow(parentWindow_);
+ return true;
+}
+
+void Engine::unloadPlugin() {
+ pluginHost_->unload();
+
+ free(inputs_[0]);
+ free(inputs_[1]);
+ free(outputs_[0]);
+ free(outputs_[1]);
+
+ inputs_[0] = nullptr;
+ inputs_[1] = nullptr;
+ outputs_[0] = nullptr;
+ outputs_[1] = nullptr;
+}
+
+void Engine::callPluginIdle() {
+ if (pluginHost_)
+ pluginHost_->idle();
+}
diff --git a/examples/host/engine.hh b/examples/host/engine.hh
@@ -0,0 +1,85 @@
+#pragma once
+
+#include <array>
+#include <memory>
+
+#include <QLibrary>
+#include <QString>
+#include <QTimer>
+#include <QWidget>
+
+#include <portaudio.h>
+#include <portmidi.h>
+#include <porttime.h>
+
+class Application;
+class Settings;
+class PluginHost;
+
+class Engine : public QObject {
+ Q_OBJECT
+
+public:
+ Engine(Application &application);
+ ~Engine();
+
+ enum State {
+ kStateStopped,
+ kStateRunning,
+ kStateStopping,
+ };
+
+ void setParentWindow(WId parentWindow) { parentWindow_ = parentWindow; }
+ void start();
+ void stop();
+
+ bool loadPlugin(const QString &path, int plugin_index);
+ void unloadPlugin();
+
+ /* send events to the plugin from GUI */
+ void setProgram(int8_t program, int8_t bank_msb, int8_t bank_lsb);
+ void loadMidiFile(const QString &path);
+
+ bool isRunning() const noexcept { return state_ == kStateRunning; }
+ int sampleRate() const noexcept { return sampleRate_; }
+
+ PluginHost &pluginHost() const { return *pluginHost_; }
+
+public:
+ void callPluginIdle();
+
+private:
+ friend class AudioPlugin;
+ friend class PluginHost;
+ friend class Vst3Plugin;
+
+ static int audioCallback(const void * input,
+ void * output,
+ unsigned long frameCount,
+ const PaStreamCallbackTimeInfo *timeInfo,
+ PaStreamCallbackFlags statusFlags,
+ void * userData);
+
+ Application &application_;
+ Settings & settings_;
+ WId parentWindow_;
+
+ State state_ = kStateStopped;
+
+ /* audio & midi streams */
+ PaStream *audio_ = nullptr;
+ PmStream *midi_ = nullptr;
+
+ /* engine context */
+ int64_t steadyTime_ = 0;
+ int32_t sampleRate_ = 44100;
+ int32_t nframes_ = 0;
+
+ /* audio buffers */
+ float *inputs_[2] = {nullptr, nullptr};
+ float *outputs_[2] = {nullptr, nullptr};
+
+ std::unique_ptr<PluginHost> pluginHost_;
+
+ QTimer idleTimer_;
+};
diff --git a/examples/host/main-window.cc b/examples/host/main-window.cc
@@ -0,0 +1,84 @@
+#include <iostream>
+
+#include <QCheckBox>
+#include <QDoubleSpinBox>
+#include <QFileDialog>
+#include <QLabel>
+#include <QLineEdit>
+#include <QMenuBar>
+#include <QToolBar>
+#include <QWindow>
+
+#include "application.hh"
+#include "engine.hh"
+#include "main-window.hh"
+#include "plugin-parameters-widget.hh"
+#include "settings-dialog.hh"
+#include "settings.hh"
+
+MainWindow::MainWindow(Application &app)
+ : QMainWindow(nullptr), application_(app),
+ settingsDialog_(new SettingsDialog(application_.settings(), this)),
+ pluginViewWindow_(new QWindow()),
+ pluginViewWidget_(QWidget::createWindowContainer(pluginViewWindow_)) {
+
+ createMenu();
+
+ setCentralWidget(pluginViewWidget_);
+ pluginViewWidget_->show();
+ pluginViewWidget_->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+ setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
+
+ connect(settingsDialog_, SIGNAL(accepted()), &application_, SLOT(restartEngine()));
+
+ pluginParametersWindow_ = new QMainWindow(this);
+ pluginParametersWidget_ =
+ new PluginParametersWidget(pluginParametersWindow_, app.engine()->pluginHost());
+ pluginParametersWindow_->setCentralWidget(pluginParametersWidget_);
+}
+
+MainWindow::~MainWindow() {}
+
+void MainWindow::createMenu() {
+ QMenuBar *menuBar = new QMenuBar(this);
+ setMenuBar(menuBar);
+
+ QMenu *fileMenu = menuBar->addMenu(tr("File"));
+ fileMenu->addAction(tr("Load plugin"));
+ connect(fileMenu->addAction(tr("Settings")),
+ &QAction::triggered,
+ this,
+ &MainWindow::showSettingsDialog);
+ fileMenu->addSeparator();
+ connect(fileMenu->addAction(tr("Quit")),
+ &QAction::triggered,
+ QApplication::instance(),
+ &Application::quit);
+
+ auto windowsMenu = menuBar->addMenu("Windows");
+ connect(windowsMenu->addAction(tr("Show Parameters")),
+ &QAction::triggered,
+ this,
+ &MainWindow::showPluginParametersWindow);
+
+ QMenu *helpMenu = menuBar->addMenu(tr("Help"));
+ helpMenu->addAction(tr("Manual"));
+ helpMenu->addAction(tr("About"));
+}
+
+void MainWindow::showSettingsDialog() {
+ int result = settingsDialog_->exec();
+ if (result == QDialog::Accepted)
+ application_.restartEngine();
+}
+
+void MainWindow::showPluginParametersWindow() { pluginParametersWindow_->show(); }
+
+WId MainWindow::getEmbedWindowId() { return pluginViewWidget_->winId(); }
+
+void MainWindow::resizePluginView(int width, int height) {
+ pluginViewWidget_->setMinimumSize(width, height);
+ pluginViewWidget_->setMaximumSize(width, height);
+ pluginViewWidget_->show();
+ adjustSize();
+}
diff --git a/examples/host/main-window.hh b/examples/host/main-window.hh
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <QMainWindow>
+
+class Application;
+class SettingsDialog;
+class PluginParametersWidget;
+
+class MainWindow : public QMainWindow {
+ Q_OBJECT
+
+public:
+ explicit MainWindow(Application &app);
+ ~MainWindow();
+
+ WId getEmbedWindowId();
+
+public:
+ void showSettingsDialog();
+ void showPluginParametersWindow();
+ void resizePluginView(int width, int height);
+
+private:
+ void createMenu();
+
+ Application & application_;
+ SettingsDialog *settingsDialog_ = nullptr;
+ QWindow * pluginViewWindow_ = nullptr;
+ QWidget * pluginViewWidget_ = nullptr;
+
+ QMainWindow * pluginParametersWindow_ = nullptr;
+ PluginParametersWidget *pluginParametersWidget_ = nullptr;
+};
diff --git a/examples/host/main.cc b/examples/host/main.cc
@@ -0,0 +1,41 @@
+#include <cstdio>
+
+#include <QApplication>
+
+#include <portaudio.h>
+#include <portmidi.h>
+
+#include "application.hh"
+
+int main(int argc, char *argv[]) {
+ PtError pt_err = Pt_Start(1, nullptr, nullptr);
+ if (pt_err != ptNoError) {
+ fprintf(stderr, "Failed to initialize porttime\n");
+ return 1;
+ }
+
+ PaError pa_err = Pa_Initialize();
+ if (pa_err != paNoError) {
+ fprintf(stderr, "Failed to initialize portaudio\n");
+ return 1;
+ }
+
+ PmError pm_err = Pm_Initialize();
+ if (pm_err != pmNoError) {
+ fprintf(stderr, "Failed to initialize portmidi\n");
+ return 1;
+ }
+
+ int ret;
+
+ {
+ Application app(argc, argv);
+
+ ret = app.exec();
+ }
+
+ Pm_Terminate();
+ Pa_Terminate();
+ Pt_Stop();
+ return ret;
+}
diff --git a/examples/host/midi-settings-widget.cc b/examples/host/midi-settings-widget.cc
@@ -0,0 +1,90 @@
+#include <iostream>
+
+#include <QComboBox>
+#include <QGroupBox>
+#include <QVBoxLayout>
+
+#include <portmidi.h>
+
+#include "midi-settings-widget.hh"
+#include "midi-settings.hh"
+
+MidiSettingsWidget::MidiSettingsWidget(MidiSettings &midiSettings) : midiSettings_(midiSettings) {
+ auto layout = new QVBoxLayout(this);
+
+ auto deviceComboBox = new QComboBox;
+ bool deviceFound = false;
+ auto deviceCount = Pm_CountDevices();
+ int inputIndex = 0;
+
+ if (deviceCount <= 0) {
+ std::cerr << "warning: no midi device found!" << std::endl;
+ }
+
+ for (int i = 0; i < deviceCount; ++i) {
+ auto deviceInfo = Pm_GetDeviceInfo(i);
+ if (!deviceInfo->input)
+ continue;
+
+ deviceComboBox->addItem(deviceInfo->name);
+
+ if (!deviceFound && midiSettings_.deviceReference().index_ == i &&
+ midiSettings_.deviceReference().name_ == deviceInfo->name) {
+ deviceComboBox->setCurrentIndex(inputIndex);
+ deviceFound = true;
+ selectedDeviceChanged(inputIndex);
+ }
+
+ ++inputIndex;
+ }
+
+ // try to find the device just by its name.
+ inputIndex = 0;
+ for (int i = 0; !deviceFound && i < deviceCount; ++i) {
+ auto deviceInfo = Pm_GetDeviceInfo(i);
+ if (!deviceInfo->input)
+ continue;
+
+ if (midiSettings_.deviceReference().name_ == deviceInfo->name) {
+ deviceComboBox->setCurrentIndex(inputIndex);
+ deviceFound = true;
+ selectedDeviceChanged(inputIndex);
+ }
+
+ ++inputIndex;
+ }
+
+ connect(
+ deviceComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(selectedDeviceChanged(int)));
+
+ layout->addWidget(deviceComboBox);
+
+ QGroupBox *groupBox = new QGroupBox;
+ groupBox->setLayout(layout);
+ groupBox->setTitle(tr("MIDI"));
+
+ QVBoxLayout *groupLayout = new QVBoxLayout;
+ groupLayout->addWidget(groupBox);
+ setLayout(groupLayout);
+}
+
+void MidiSettingsWidget::selectedDeviceChanged(int index) {
+ int inputIndex = 0;
+ auto deviceCount = Pm_CountDevices();
+ for (int i = 0; i < deviceCount; ++i) {
+ auto deviceInfo = Pm_GetDeviceInfo(i);
+ if (!deviceInfo->input)
+ continue;
+
+ if (inputIndex != index) {
+ ++inputIndex;
+ continue;
+ }
+
+ DeviceReference ref;
+ ref.index_ = i;
+ ref.name_ = deviceInfo->name;
+ midiSettings_.setDeviceReference(ref);
+ break;
+ }
+}
diff --git a/examples/host/midi-settings-widget.hh b/examples/host/midi-settings-widget.hh
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <vector>
+
+#include <QWidget>
+
+class MidiSettings;
+
+class MidiSettingsWidget : public QWidget {
+ Q_OBJECT
+public:
+ explicit MidiSettingsWidget(MidiSettings &midiSettings);
+
+signals:
+
+public slots:
+ void selectedDeviceChanged(int index);
+
+private:
+ MidiSettings &midiSettings_;
+};
diff --git a/examples/host/midi-settings.cc b/examples/host/midi-settings.cc
@@ -0,0 +1,20 @@
+#include <QSettings>
+
+#include "midi-settings.hh"
+
+static const char DEVICE_NAME_KEY[] = "Midi/DeviceName";
+static const char DEVICE_INDEX_KEY[] = "Midi/DeviceIndex";
+static const char LATCH_KEY[] = "Midi/Latch";
+static const char ARP_KEY[] = "Midi/Arp";
+
+MidiSettings::MidiSettings() {}
+
+void MidiSettings::load(QSettings &settings) {
+ deviceReference_.name_ = settings.value(DEVICE_NAME_KEY).toString();
+ deviceReference_.index_ = settings.value(DEVICE_INDEX_KEY).toInt();
+}
+
+void MidiSettings::save(QSettings &settings) const {
+ settings.setValue(DEVICE_NAME_KEY, deviceReference_.name_);
+ settings.setValue(DEVICE_INDEX_KEY, deviceReference_.index_);
+}
diff --git a/examples/host/midi-settings.hh b/examples/host/midi-settings.hh
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "device-reference.hh"
+
+class QSettings;
+
+class MidiSettings {
+public:
+ MidiSettings();
+
+ void load(QSettings &settings);
+ void save(QSettings &settings) const;
+
+ const DeviceReference &deviceReference() const { return deviceReference_; }
+ void setDeviceReference(const DeviceReference &ref) { deviceReference_ = ref; }
+
+private:
+ DeviceReference deviceReference_;
+};
diff --git a/examples/host/param-queue.cc b/examples/host/param-queue.cc
@@ -0,0 +1,49 @@
+#include <QDebug>
+
+#include "param-queue.hh"
+
+ParamQueue::ParamQueue() { reset(); }
+
+void ParamQueue::reset() {
+ for (auto &q : queues_)
+ q.clear();
+
+ free_ = &queues_[0];
+ producer_ = &queues_[1];
+ consumer_ = nullptr;
+}
+
+void ParamQueue::setCapacity(size_t capacity) {
+ for (auto &q : queues_)
+ q.reserve(2 * capacity);
+}
+
+void ParamQueue::set(clap_id id, clap_param_value value) { producer_.load()->emplace(id, value); }
+
+void ParamQueue::producerDone() {
+ if (consumer_)
+ return;
+
+ consumer_.store(producer_.load());
+ producer_.store(free_.load());
+ free_.store(nullptr);
+
+ Q_ASSERT(producer_);
+}
+
+void ParamQueue::consume(const std::function<void(clap_id, clap_param_value)> consumer) {
+ Q_ASSERT(consumer);
+
+ if (!consumer_)
+ return;
+
+ for (auto &x : *consumer_)
+ consumer(x.first, x.second);
+
+ consumer_.load()->clear();
+ if (free_)
+ return;
+
+ free_ = consumer_.load();
+ consumer_ = nullptr;
+}
diff --git a/examples/host/param-queue.hh b/examples/host/param-queue.hh
@@ -0,0 +1,29 @@
+#pragma once
+
+#include <atomic>
+#include <functional>
+#include <unordered_map>
+
+#include <clap/all.h>
+
+class ParamQueue {
+public:
+ using queue_type = std::unordered_map<clap_id, clap_param_value>;
+
+ ParamQueue();
+
+ void setCapacity(size_t capacity);
+
+ void set(clap_id id, clap_param_value value);
+ void producerDone();
+
+ void consume(const std::function<void(clap_id id, clap_param_value value)> consumer);
+
+ void reset();
+
+private:
+ queue_type queues_[2];
+ std::atomic<queue_type *> free_ = nullptr;
+ std::atomic<queue_type *> producer_ = nullptr;
+ std::atomic<queue_type *> consumer_ = nullptr;
+};
+\ No newline at end of file
diff --git a/examples/host/plugin-host.cc b/examples/host/plugin-host.cc
@@ -0,0 +1,938 @@
+#include <exception>
+#include <iostream>
+#include <memory>
+#include <sstream>
+#include <stdexcept>
+#include <string_view>
+#include <unordered_set>
+
+#include <QDebug>
+
+#include "application.hh"
+#include "engine.hh"
+#include "main-window.hh"
+#include "plugin-host.hh"
+
+enum ThreadType {
+ Unknown,
+ MainThread,
+ AudioThread,
+ AudioThreadPool,
+};
+
+thread_local ThreadType g_thread_type = Unknown;
+
+PluginHost::PluginHost(Engine &engine) : QObject(&engine), engine_(engine) {
+ g_thread_type = MainThread;
+
+ host_.host_data = this;
+ host_.clap_version = CLAP_VERSION;
+ host_.extension = PluginHost::clapHostExtension;
+ host_.name = "Mini Test Host";
+ host_.version = "0.0.1";
+ host_.vendor = "u-he";
+ host_.url = "https://www.u-he.com";
+
+ hostLog_.log = PluginHost::clapHostLog;
+
+ hostGui_.resize = PluginHost::clapHostGuiResize;
+
+ hostThreadCheck_.is_main_thread = PluginHost::clapIsMainThread;
+ hostThreadCheck_.is_audio_thread = PluginHost::clapIsAudioThread;
+
+ hostThreadPool_.request_exec = PluginHost::clapThreadPoolRequestExec;
+
+ hostEventLoop_.register_timer = PluginHost::clapEventLoopRegisterTimer;
+ hostEventLoop_.unregister_timer = PluginHost::clapEventLoopUnregisterTimer;
+ hostEventLoop_.register_fd = PluginHost::clapEventLoopRegisterFd;
+ hostEventLoop_.modify_fd = PluginHost::clapEventLoopModifyFd;
+ hostEventLoop_.unregister_fd = PluginHost::clapEventLoopUnregisterFd;
+
+ hostParams_.adjust_begin = PluginHost::clapParamsAdjustBegin;
+ hostParams_.adjust_end = PluginHost::clapParamsAdjustEnd;
+ hostParams_.adjust = PluginHost::clapParamsAdjust;
+ hostParams_.rescan = PluginHost::clapParamsRescan;
+
+ initThreadPool();
+}
+
+PluginHost::~PluginHost() {
+ checkForMainThread();
+
+ terminateThreadPool();
+}
+
+void PluginHost::initThreadPool() {
+ checkForMainThread();
+
+ threadPoolStop_ = false;
+ threadPoolTaskIndex_ = 0;
+ auto N = QThread::idealThreadCount();
+ threadPool_.resize(N);
+ for (int i = 0; i < N; ++i) {
+ threadPool_[i].reset(QThread::create(&PluginHost::threadPoolEntry, this));
+ threadPool_[i]->start(QThread::HighestPriority);
+ }
+}
+
+void PluginHost::terminateThreadPool() {
+ checkForMainThread();
+
+ threadPoolStop_ = true;
+ threadPoolSemaphoreProd_.release(threadPool_.size());
+ for (auto &thr : threadPool_)
+ if (thr)
+ thr->wait();
+}
+
+void PluginHost::threadPoolEntry() {
+ g_thread_type = AudioThreadPool;
+ while (true) {
+ threadPoolSemaphoreProd_.acquire();
+ if (threadPoolStop_)
+ return;
+
+ int taskIndex = threadPoolTaskIndex_++;
+ pluginThreadPool_->exec(plugin_, taskIndex);
+ threadPoolSemaphoreDone_.release();
+ }
+}
+
+bool PluginHost::load(const QString &path, int pluginIndex) {
+ checkForMainThread();
+
+ if (library_.isLoaded())
+ unload();
+
+ library_.setFileName(path);
+ if (!library_.load()) {
+ QString err = library_.errorString();
+ qWarning() << "failed to load " << path << ": " << err;
+ return false;
+ }
+
+ pluginEntry_ =
+ reinterpret_cast<const struct clap_plugin_entry *>(library_.resolve("clap_plugin_entry"));
+ if (!pluginEntry_) {
+ library_.unload();
+ return false;
+ }
+
+ pluginEntry_->init(path.toStdString().c_str());
+
+ auto count = pluginEntry_->get_plugin_count();
+ if (pluginIndex > count) {
+ qWarning() << "plugin index greater than count :" << count;
+ return false;
+ }
+
+ auto desc = pluginEntry_->get_plugin_descriptor(pluginIndex);
+ if (!desc) {
+ qWarning() << "no plugin descriptor";
+ return false;
+ }
+
+ plugin_ = pluginEntry_->create_plugin(&host_, desc->id);
+ if (!plugin_) {
+ qWarning() << "could not create the plugin with id: " << desc->id;
+ return false;
+ }
+ plugin_->init(plugin_);
+
+ initPluginExtensions();
+ scanParams();
+ return true;
+}
+
+void PluginHost::initPluginExtensions() {
+ if (pluginExtensionsAreInitialized_)
+ return;
+
+ initPluginExtension(pluginParams_, CLAP_EXT_PARAMS);
+ initPluginExtension(pluginAudioPorts_, CLAP_EXT_AUDIO_PORTS);
+ initPluginExtension(pluginGui_, CLAP_EXT_GUI);
+ initPluginExtension(pluginGuiX11_, CLAP_EXT_GUI_X11);
+ initPluginExtension(pluginGuiWin32_, CLAP_EXT_GUI_WIN32);
+ initPluginExtension(pluginGuiCocoa_, CLAP_EXT_GUI_COCOA);
+ initPluginExtension(pluginGuiFreeStanding_, CLAP_EXT_GUI_FREE_STANDING);
+ initPluginExtension(pluginEventLoop_, CLAP_EXT_EVENT_LOOP);
+ initPluginExtension(pluginThreadPool_, CLAP_EXT_THREAD_POOL);
+
+ pluginExtensionsAreInitialized_ = true;
+}
+
+void PluginHost::unload() {
+ checkForMainThread();
+
+ if (!library_.isLoaded())
+ return;
+
+ if (pluginGui_)
+ pluginGui_->close(plugin_);
+
+ if (isPluginActive()) {
+ plugin_->set_active(plugin_, 0, false);
+ setPluginState(Inactive);
+ }
+
+ plugin_->destroy(plugin_);
+ plugin_ = nullptr;
+ pluginGui_ = nullptr;
+ pluginAudioPorts_ = nullptr;
+
+ pluginEntry_->deinit();
+ pluginEntry_ = nullptr;
+
+ library_.unload();
+}
+
+bool PluginHost::canActivate() const {
+ if (!engine_.isRunning())
+ return false;
+ if (isPluginActive())
+ return false;
+ if (scheduleDeactivateForParameterScan_)
+ return false;
+ return true;
+}
+
+void PluginHost::activate(int32_t sample_rate) {
+ if (plugin_->set_active(plugin_, sample_rate, true))
+ setPluginState(ActiveAndSleeping);
+ else
+ setPluginState(InactiveWithError);
+}
+
+void PluginHost::setPorts(int numInputs, float **inputs, int numOutputs, float **outputs) {
+ audioIn_.channel_count = numInputs;
+ audioIn_.data32 = inputs;
+ audioIn_.data64 = nullptr;
+ audioIn_.constant_mask = 0;
+ audioIn_.latency = 0;
+
+ audioOut_.channel_count = numOutputs;
+ audioOut_.data32 = outputs;
+ audioOut_.data64 = nullptr;
+ audioOut_.constant_mask = 0;
+ audioOut_.latency = 0;
+}
+
+void PluginHost::setParentWindow(WId parentWindow) {
+ checkForMainThread();
+
+#if defined(Q_OS_LINUX)
+ if (pluginGuiX11_)
+ pluginGuiX11_->attach(plugin_, nullptr, parentWindow);
+#elif defined(Q_OS_MACX)
+ if (pluginEmbedCocoa_)
+ pluginGuiCocoa_->attach(plugin_, (void *)parentWindow);
+#elif defined(Q_OS_WIN32)
+ if (pluginEmbedWin32_)
+ pluginGuiWin32_->attach(plugin_, parentWindow);
+#endif
+ // else (pluginGuiFreeStanding_)
+ // pluginGuiFreeStanding_->open(plugin_);
+
+ int width = 0;
+ int height = 0;
+
+ if (pluginGui_)
+ pluginGui_->get_size(plugin_, &width, &height);
+
+ Application::instance().mainWindow()->resizePluginView(width, height);
+}
+
+void PluginHost::clapHostLog(clap_host *host, clap_log_severity severity, const char *msg) {
+ switch (severity) {
+ case CLAP_LOG_DEBUG:
+ qDebug() << msg;
+ break;
+
+ case CLAP_LOG_INFO:
+ qInfo() << msg;
+ break;
+
+ case CLAP_LOG_WARNING:
+ case CLAP_LOG_ERROR:
+ case CLAP_LOG_FATAL:
+ case CLAP_LOG_HOST_MISBEHAVING:
+ qWarning() << msg;
+ break;
+ }
+}
+
+template <typename T>
+void PluginHost::initPluginExtension(const T *&ext, const char *id) {
+ if (ext)
+ return;
+
+ checkForMainThread();
+ ext = static_cast<const T *>(plugin_->extension(plugin_, id));
+}
+
+const void *PluginHost::clapHostExtension(clap_host *host, const char *extension) {
+ checkForMainThread();
+
+ PluginHost *h = static_cast<PluginHost *>(host->host_data);
+ if (!h->plugin_)
+ throw std::logic_error("The plugin can't query for extensions during the create method. Wait "
+ "for clap_plugin.init() call.");
+
+ if (!strcmp(extension, CLAP_EXT_GUI))
+ return &h->hostGui_;
+ if (!strcmp(extension, CLAP_EXT_LOG))
+ return &h->hostLog_;
+ if (!strcmp(extension, CLAP_EXT_THREAD_CHECK))
+ return &h->hostThreadCheck_;
+ if (!strcmp(extension, CLAP_EXT_EVENT_LOOP))
+ return &h->hostEventLoop_;
+ if (!strcmp(extension, CLAP_EXT_PARAMS))
+ return &h->hostParams_;
+
+ return nullptr;
+}
+
+PluginHost *PluginHost::fromHost(clap_host *host) {
+ if (!host)
+ throw std::invalid_argument("Passed a null host pointer");
+
+ auto h = static_cast<PluginHost *>(host->host_data);
+ if (!h)
+ throw std::invalid_argument("Passed an invalid host pointer because the host_data is null");
+
+ if (!h->plugin_)
+ throw std::logic_error(
+ "Called into host interfaces befores the host knows the plugin pointer");
+
+ return h;
+}
+
+bool PluginHost::clapIsMainThread(clap_host *host) { return g_thread_type == MainThread; }
+
+bool PluginHost::clapIsAudioThread(clap_host *host) { return g_thread_type == AudioThread; }
+
+void PluginHost::checkForMainThread() {
+ if (g_thread_type != MainThread)
+ throw std::logic_error("Requires Main Thread!");
+}
+
+void PluginHost::checkForAudioThread() {
+ if (g_thread_type != AudioThread)
+ throw std::logic_error("Requires Audio Thread!");
+}
+
+bool PluginHost::clapThreadPoolRequestExec(clap_host *host, uint32_t num_tasks) {
+ checkForAudioThread();
+
+ auto h = fromHost(host);
+ if (!h->pluginThreadPool_ || !h->pluginThreadPool_->exec)
+ throw std::logic_error("Called request_exec() without providing clap_plugin_thread_pool to "
+ "execute the job.");
+
+ Q_ASSERT(!h->threadPoolStop_);
+ Q_ASSERT(!h->threadPool_.empty());
+ h->threadPoolTaskIndex_ = 0;
+ h->threadPoolSemaphoreProd_.release(num_tasks);
+ h->threadPoolSemaphoreDone_.acquire(num_tasks);
+ return true;
+}
+
+bool PluginHost::clapEventLoopRegisterTimer(clap_host *host,
+ uint32_t period_ms,
+ clap_id * timer_id) {
+ checkForMainThread();
+
+ auto h = fromHost(host);
+ h->initPluginExtensions();
+ if (!h->pluginEventLoop_ || !h->pluginEventLoop_->on_timer)
+ throw std::logic_error("Called register_timer() without providing clap_plugin_event_loop to "
+ "receive the timer event.");
+
+ auto id = h->nextTimerId_++;
+ *timer_id = id;
+ auto timer = std::make_unique<QTimer>();
+
+ QObject::connect(timer.get(), &QTimer::timeout, [h, id] {
+ checkForMainThread();
+ h->pluginEventLoop_->on_timer(h->plugin_, id);
+ });
+
+ auto t = timer.get();
+ h->timers_.emplace(*timer_id, std::move(timer));
+ t->start(period_ms);
+ return true;
+}
+
+bool PluginHost::clapEventLoopUnregisterTimer(clap_host *host, clap_id timer_id) {
+ checkForMainThread();
+
+ auto h = fromHost(host);
+ if (!h->pluginEventLoop_ || !h->pluginEventLoop_->on_timer)
+ throw std::logic_error(
+ "Called unregister_timer() without providing clap_plugin_event_loop to "
+ "receive the timer event.");
+
+ auto it = h->timers_.find(timer_id);
+ if (it == h->timers_.end())
+ throw std::logic_error("Called unregister_timer() for a timer_id that was not registered.");
+
+ h->timers_.erase(it);
+ return true;
+}
+
+bool PluginHost::clapEventLoopRegisterFd(clap_host *host, clap_fd fd, uint32_t flags) {
+ checkForMainThread();
+
+ auto h = fromHost(host);
+ h->initPluginExtensions();
+ if (!h->pluginEventLoop_ || !h->pluginEventLoop_->on_fd)
+ throw std::logic_error(
+ "Called unregister_timer() without providing clap_plugin_event_loop to "
+ "receive the timer event.");
+
+ auto it = h->fds_.find(fd);
+ if (it != h->fds_.end())
+ throw std::logic_error(
+ "Called register_fd() for a fd that was already registered, use modify_fd() instead.");
+
+ h->fds_.emplace(fd, std::make_unique<Notifiers>());
+ h->eventLoopSetFdNotifierFlags(fd, flags);
+ return true;
+}
+
+bool PluginHost::clapEventLoopModifyFd(clap_host *host, clap_fd fd, uint32_t flags) {
+ checkForMainThread();
+
+ auto h = fromHost(host);
+ if (!h->pluginEventLoop_ || !h->pluginEventLoop_->on_fd)
+ throw std::logic_error(
+ "Called unregister_timer() without providing clap_plugin_event_loop to "
+ "receive the timer event.");
+
+ auto it = h->fds_.find(fd);
+ if (it == h->fds_.end())
+ throw std::logic_error(
+ "Called modify_fd() for a fd that was not registered, use register_fd() instead.");
+
+ h->fds_.emplace(fd, std::make_unique<Notifiers>());
+ h->eventLoopSetFdNotifierFlags(fd, flags);
+ return true;
+}
+
+bool PluginHost::clapEventLoopUnregisterFd(clap_host *host, clap_fd fd) {
+ checkForMainThread();
+
+ auto h = fromHost(host);
+ if (!h->pluginEventLoop_ || !h->pluginEventLoop_->on_fd)
+ throw std::logic_error(
+ "Called unregister_timer() without providing clap_plugin_event_loop to "
+ "receive the timer event.");
+
+ auto it = h->fds_.find(fd);
+ if (it == h->fds_.end())
+ throw std::logic_error("Called unregister_fd() for a fd that was not registered.");
+
+ h->fds_.erase(it);
+ return true;
+}
+
+void PluginHost::eventLoopSetFdNotifierFlags(clap_fd fd, uint32_t flags) {
+ checkForMainThread();
+
+ auto it = fds_.find(fd);
+ Q_ASSERT(it != fds_.end());
+
+ if (flags & CLAP_FD_READ) {
+ if (!it->second->rd) {
+ it->second->rd.reset(new QSocketNotifier(fd, QSocketNotifier::Read));
+ QObject::connect(it->second->rd.get(), &QSocketNotifier::activated, [this, fd] {
+ checkForMainThread();
+ this->pluginEventLoop_->on_fd(this->plugin_, fd, CLAP_FD_READ);
+ });
+ }
+ it->second->rd->setEnabled(true);
+ } else if (it->second->rd)
+ it->second->rd->setEnabled(false);
+
+ if (flags & CLAP_FD_WRITE) {
+ if (!it->second->wr) {
+ it->second->wr.reset(new QSocketNotifier(fd, QSocketNotifier::Write));
+ QObject::connect(it->second->wr.get(), &QSocketNotifier::activated, [this, fd] {
+ checkForMainThread();
+ this->pluginEventLoop_->on_fd(this->plugin_, fd, CLAP_FD_WRITE);
+ });
+ }
+ it->second->wr->setEnabled(true);
+ } else if (it->second->wr)
+ it->second->wr->setEnabled(false);
+
+ if (flags & CLAP_FD_ERROR) {
+ if (!it->second->err) {
+ it->second->err.reset(new QSocketNotifier(fd, QSocketNotifier::Exception));
+ QObject::connect(it->second->err.get(), &QSocketNotifier::activated, [this, fd] {
+ checkForMainThread();
+ this->pluginEventLoop_->on_fd(this->plugin_, fd, CLAP_FD_ERROR);
+ });
+ }
+ it->second->err->setEnabled(true);
+ } else if (it->second->err)
+ it->second->err->setEnabled(false);
+}
+
+bool PluginHost::clapHostGuiResize(clap_host *host, int32_t width, int32_t height) {
+ checkForMainThread();
+
+ PluginHost *h = static_cast<PluginHost *>(host->host_data);
+
+ Application::instance().mainWindow()->resizePluginView(width, height);
+ return true;
+}
+
+void PluginHost::processInit(int nframes) {
+ process_.frames_count = nframes;
+ process_.steady_time = engine_.steadyTime_;
+}
+
+void PluginHost::processNoteOn(int sampleOffset, int channel, int key, int velocity) {
+ clap_event ev;
+
+ ev.type = CLAP_EVENT_NOTE_ON;
+ ev.time = sampleOffset;
+ ev.note.key = key;
+ ev.note.channel = channel;
+ ev.note.velocity = velocity / 127.0;
+
+ evIn_.push_back(ev);
+}
+
+void PluginHost::processNoteOff(int sampleOffset, int channel, int key, int velocity) {
+ clap_event ev;
+
+ ev.type = CLAP_EVENT_NOTE_OFF;
+ ev.time = sampleOffset;
+ ev.note.key = key;
+ ev.note.channel = channel;
+ ev.note.velocity = velocity / 127.0;
+
+ evIn_.push_back(ev);
+}
+
+void PluginHost::processNoteAt(int sampleOffset, int channel, int key, int pressure) {
+ // TODO
+}
+
+void PluginHost::processPitchBend(int sampleOffset, int channel, int value) {
+ // TODO
+}
+
+void PluginHost::processCC(int sampleOffset, int channel, int cc, int value) {
+ clap_event ev;
+
+ ev.type = CLAP_EVENT_MIDI;
+ ev.time = sampleOffset;
+ ev.midi.data[0] = 0;
+ ev.midi.data[1] = 0xB0 | channel;
+ ev.midi.data[2] = cc;
+ ev.midi.data[3] = value;
+
+ evIn_.push_back(ev);
+}
+
+static uint32_t clap_host_event_list_size(const struct clap_event_list *list) {
+ PluginHost::checkForAudioThread();
+
+ auto vec = reinterpret_cast<std::vector<clap_event> *>(list->ctx);
+ return vec->size();
+}
+
+const struct clap_event *clap_host_event_list_get(const struct clap_event_list *list,
+ uint32_t index) {
+ PluginHost::checkForAudioThread();
+
+ auto vec = reinterpret_cast<std::vector<clap_event> *>(list->ctx);
+ if (index < 0 || index >= vec->size())
+ return nullptr;
+ return vec->data() + index;
+}
+
+// Makes a copy of the event
+void clap_host_event_list_push_back(const struct clap_event_list *list,
+ const struct clap_event * event) {
+ PluginHost::checkForAudioThread();
+
+ auto vec = reinterpret_cast<std::vector<clap_event> *>(list->ctx);
+ vec->push_back(*event);
+}
+
+void PluginHost::process() {
+ g_thread_type = AudioThread;
+
+ if (!isPluginActive())
+ return;
+
+ process_.transport = nullptr;
+
+ clap_event_list in_ev = {
+ &evIn_, clap_host_event_list_size, clap_host_event_list_get, clap_host_event_list_push_back};
+
+ clap_event_list out_ev = {
+ &evOut_, clap_host_event_list_size, clap_host_event_list_get, clap_host_event_list_push_back};
+
+ process_.in_events = &in_ev;
+ process_.out_events = &out_ev;
+
+ process_.audio_inputs = &audioIn_;
+ process_.audio_inputs_count = 1;
+ process_.audio_outputs = &audioOut_;
+ process_.audio_outputs_count = 1;
+
+ evOut_.clear();
+ appToEngineQueue_.consume([this](clap_id param_id, clap_param_value value) {
+ clap_event ev;
+ ev.time = 0;
+ ev.type = CLAP_EVENT_PARAM_SET;
+ ev.param.param_id = param_id;
+ ev.param.key = -1;
+ ev.param.channel = -1;
+ ev.param.ramp = 0;
+ ev.param.value = value;
+ evIn_.push_back(ev);
+ });
+
+ // TODO if the plugin was not processing and had audio or events that should
+ // wake it, then we should set it as processing
+ if (!isPluginProcessing()) {
+ plugin_->set_processing(plugin_, true);
+ setPluginState(ActiveAndProcessing);
+ }
+
+ int32_t status;
+ if (plugin_ && plugin_->process)
+ status = plugin_->process(plugin_, &process_);
+
+ for (auto &ev : evOut_) {
+ switch (ev.type) {
+ case CLAP_EVENT_PARAM_SET:
+ engineToAppQueue_.set(ev.param.param_id, ev.param.value);
+ break;
+ }
+ }
+ evOut_.clear();
+ evIn_.clear();
+
+ if (scheduleDeactivateForParameterScan_) {
+ plugin_->set_processing(plugin_, false);
+ setPluginState(ActiveAndReadyToDeactivate);
+ }
+
+ engineToAppQueue_.producerDone();
+ g_thread_type = Unknown;
+}
+
+void PluginHost::idle() {
+ checkForMainThread();
+
+ // Try to send events to the audio engine
+ appToEngineQueue_.producerDone();
+ engineToAppQueue_.consume([this](clap_id param_id, clap_param_value value) {
+ auto it = params_.find(param_id);
+ if (it == params_.end()) {
+ std::ostringstream msg;
+ msg << "Plugin produced a CLAP_EVENT_PARAM_SET with an unknown param_id: " << param_id;
+ throw std::invalid_argument(msg.str());
+ }
+
+ it->second->setValue(value);
+ if (pluginParams_ && pluginParams_->set_value)
+ pluginParams_->set_value(plugin_, param_id, value, value);
+ });
+}
+
+PluginParam &PluginHost::checkValidParamId(const std::string_view &function,
+ const std::string_view ¶m_name,
+ clap_id param_id) {
+ checkForMainThread();
+
+ if (param_id == CLAP_INVALID_ID) {
+ std::ostringstream msg;
+ msg << "Plugin called " << function << " with " << param_name << " == CLAP_INVALID_ID";
+ throw std::invalid_argument(msg.str());
+ }
+
+ auto it = params_.find(param_id);
+ if (it == params_.end()) {
+ std::ostringstream msg;
+ msg << "Plugin called " << function << " with an invalid " << param_name
+ << " == " << param_id;
+ throw std::invalid_argument(msg.str());
+ }
+
+ Q_ASSERT(it->first == param_id);
+ Q_ASSERT(it->second->info().id == param_id);
+ return *it->second;
+}
+
+void PluginHost::checkValidParamValue(const PluginParam ¶m, clap_param_value value) {
+ checkForMainThread();
+ if (!param.isValueValid(value)) {
+ std::ostringstream msg;
+ msg << "Invalid value for param. ";
+ param.printInfo(msg);
+ msg << "; value: ";
+ param.printValue(value, msg);
+ // std::cerr << msg.str() << std::endl;
+ throw std::invalid_argument(msg.str());
+ }
+}
+
+void PluginHost::clapParamsAdjustBegin(clap_host *host, clap_id param_id) {
+ checkForMainThread();
+
+ auto h = fromHost(host);
+ auto ¶m = h->checkValidParamId("clap_host_params.touch_begin()", "param_id", param_id);
+
+ if (param.isBeingAdjusted()) {
+ std::ostringstream msg;
+ msg << "Plugin called clap_host_params.adjust_end() on param_id: " << param_id
+ << ", but this parameter is already marked as being adjusted";
+ throw std::logic_error(msg.str());
+ }
+
+ param.beginAdjust();
+}
+
+void PluginHost::clapParamsAdjustEnd(clap_host *host, clap_id param_id) {
+ checkForMainThread();
+
+ auto h = fromHost(host);
+ auto ¶m = h->checkValidParamId("clap_host_params.touch_begin()", "param_id", param_id);
+
+ if (!param.isBeingAdjusted()) {
+ std::ostringstream msg;
+ msg << "Plugin called clap_host_params.adjust_end() on param_id: " << param_id
+ << ", but this parameter is not marked as being adjusted";
+ throw std::logic_error(msg.str());
+ }
+
+ param.endAdjust();
+}
+
+void PluginHost::clapParamsAdjust(clap_host *host, clap_id param_id, clap_param_value value) {
+ checkForMainThread();
+
+ auto h = fromHost(host);
+ auto ¶m = h->checkValidParamId("clap_host_params.touch_begin()", "param_id", param_id);
+
+ if (!param.isBeingAdjusted()) {
+ std::ostringstream msg;
+ msg << "Plugin called clap_host_params.adjust() on param_id: " << param_id
+ << ", but this parameter is not marked as being adjusted";
+ throw std::logic_error(msg.str());
+ }
+
+ h->checkValidParamValue(param, value);
+
+ if (param.isValueEqualTo(value))
+ return;
+
+ param.setValue(value);
+ h->appToEngineQueue_.set(param_id, value);
+ h->pluginParams_->set_value(h->plugin_, param.info().id, value, value);
+ h->appToEngineQueue_.producerDone();
+}
+
+void PluginHost::setParamValueByHost(PluginParam ¶m, clap_param_value value) {
+ checkForMainThread();
+
+ param.setValue(value);
+ appToEngineQueue_.set(param.info().id, value);
+ if (pluginParams_ && pluginParams_->set_value)
+ pluginParams_->set_value(plugin_, param.info().id, value, value);
+ appToEngineQueue_.producerDone();
+}
+
+void PluginHost::scanParams() { clapParamsRescan(&host_, CLAP_PARAM_RESCAN_ALL); }
+
+void PluginHost::clapParamsRescan(clap_host *host, uint32_t flags) {
+ checkForMainThread();
+ auto h = fromHost(host);
+
+ // 1. if the plugin is activated, check if we need to deactivate it
+ if (h->isPluginActive() && (flags & CLAP_PARAM_RESCAN_ALL)) {
+ h->scheduleDeactivateForParameterScan_ = true;
+ h->scheduleParamsRescanFlags_ |= flags;
+ return;
+ }
+
+ // 2. scan the params.
+ auto count = h->pluginParams_->count(h->plugin_);
+ std::unordered_set<clap_id> paramIds(count * 2);
+
+ for (int32_t i = 0; i < count; ++i) {
+ clap_param_info info;
+ if (!h->pluginParams_->get_info(h->plugin_, i, &info))
+ throw std::logic_error("clap_plugin_params.get_info did return false!");
+
+ if (info.id == CLAP_INVALID_ID) {
+ std::ostringstream msg;
+ msg << "clap_plugin_params.get_info() reported a parameter with id = CLAP_INVALID_ID"
+ << std::endl
+ << " 2. name: " << info.name << ", module: " << info.module << std::endl;
+ throw std::logic_error(msg.str());
+ }
+
+ auto it = h->params_.find(info.id);
+
+ // check that the parameter is not declared twice
+ if (paramIds.count(info.id) > 0) {
+ Q_ASSERT(it != h->params_.end());
+
+ std::ostringstream msg;
+ msg << "the parameter with id: " << info.id << " was declared twice." << std::endl
+ << " 1. name: " << it->second->info().name << ", module: " << it->second->info().module
+ << std::endl
+ << " 2. name: " << info.name << ", module: " << info.module << std::endl;
+ throw std::logic_error(msg.str());
+ }
+ paramIds.insert(info.id);
+
+ if (it == h->params_.end()) {
+ if (!(flags & CLAP_PARAM_RESCAN_ALL)) {
+ std::ostringstream msg;
+ msg << "a new parameter was declared, but the flag CLAP_PARAM_RESCAN_ALL was not "
+ "specified; id: "
+ << info.id << ", name: " << info.name << ", module: " << info.module << std::endl;
+ throw std::logic_error(msg.str());
+ }
+
+ clap_param_value value = h->getParamValue(info);
+ auto param = std::make_unique<PluginParam>(*h, info, value);
+ h->checkValidParamValue(*param, value);
+ h->params_.emplace(info.id, std::move(param));
+ } else {
+ // update param info
+ if (!it->second->isInfoEqualTo(info)) {
+ if (!clapParamsRescanMayInfoChange(flags)) {
+ std::ostringstream msg;
+ msg << "a parameter's info did change, but the flag CLAP_PARAM_RESCAN_INFO "
+ "was not specified; id: "
+ << info.id << ", name: " << info.name << ", module: " << info.module
+ << std::endl;
+ throw std::logic_error(msg.str());
+ }
+
+ if (!(flags & CLAP_PARAM_RESCAN_ALL) &&
+ !it->second->isInfoCriticallyDifferentTo(info)) {
+ std::ostringstream msg;
+ msg << "a parameter's info has critical changes, but the flag CLAP_PARAM_RESCAN_ALL "
+ "was not specified; id: "
+ << info.id << ", name: " << info.name << ", module: " << info.module
+ << std::endl;
+ throw std::logic_error(msg.str());
+ }
+
+ it->second->setInfo(info);
+ }
+
+ clap_param_value value = h->getParamValue(info);
+ if (!it->second->isValueEqualTo(value)) {
+ if (!clapParamsRescanMayValueChange(flags)) {
+ std::ostringstream msg;
+ msg << "a parameter's value did change but, but the flag CLAP_PARAM_RESCAN_VALUES "
+ "was not specified; id: "
+ << info.id << ", name: " << info.name << ", module: " << info.module
+ << std::endl;
+ throw std::logic_error(msg.str());
+ }
+
+ // update param value
+ h->checkValidParamValue(*it->second, value);
+ it->second->setValue(value);
+ it->second->setModulatedValue(value);
+ }
+ }
+ }
+
+ // remove parameters which are gone
+ for (auto it = h->params_.begin(); it != h->params_.end();) {
+ if (paramIds.find(it->first) != paramIds.end())
+ ++it;
+ else {
+ if (!(flags & CLAP_PARAM_RESCAN_ALL)) {
+ std::ostringstream msg;
+ auto & info = it->second->info();
+ msg << "a parameter was removed, but the flag CLAP_PARAM_RESCAN_ALL was not "
+ "specified; id: "
+ << info.id << ", name: " << info.name << ", module: " << info.module << std::endl;
+ throw std::logic_error(msg.str());
+ }
+ it = h->params_.erase(it);
+ }
+ }
+
+ if (flags & CLAP_PARAM_RESCAN_ALL) {
+ h->scheduleDeactivateForParameterScan_ = false;
+ h->scheduleParamsRescanFlags_ = 0;
+
+ if (h->canActivate())
+ h->plugin_->set_active(h->plugin_, h->engine_.sampleRate(), true);
+
+ h->paramsChanged();
+ }
+}
+
+clap_param_value PluginHost::getParamValue(const clap_param_info &info) {
+ clap_param_value value;
+ if (pluginParams_->get_value(plugin_, info.id, &value))
+ return value;
+
+ std::ostringstream msg;
+ msg << "failed to get the param value, id: " << info.id << ", name: " << info.name
+ << ", module: " << info.module;
+ throw std::logic_error(msg.str());
+}
+
+void PluginHost::setPluginState(PluginState state) {
+ switch (state) {
+ case Inactive:
+ Q_ASSERT(state_ == ActiveAndReadyToDeactivate);
+ break;
+
+ case InactiveWithError:
+ Q_ASSERT(state_ == Inactive);
+ break;
+
+ case ActiveAndSleeping:
+ Q_ASSERT(state_ == Inactive || state_ == ActiveAndProcessing);
+ break;
+
+ case ActiveAndProcessing:
+ Q_ASSERT(state_ == ActiveAndSleeping);
+ break;
+
+ case ActiveWithError:
+ Q_ASSERT(state_ == ActiveAndProcessing);
+ break;
+
+ case ActiveAndReadyToDeactivate:
+ Q_ASSERT(state_ == ActiveAndSleeping || state_ == ActiveWithError);
+ break;
+
+ default:
+ std::terminate();
+ }
+
+ state_ = state;
+}
+
+bool PluginHost::isPluginActive() const {
+ switch (state_) {
+ case Inactive:
+ case InactiveWithError:
+ return false;
+ default:
+ return true;
+ }
+}
+
+bool PluginHost::isPluginProcessing() const { return state_ == ActiveAndProcessing; }
+\ No newline at end of file
diff --git a/examples/host/plugin-host.hh b/examples/host/plugin-host.hh
@@ -0,0 +1,196 @@
+#pragma once
+
+#include <array>
+#include <memory>
+#include <unordered_map>
+#include <unordered_set>
+
+#include <QLibrary>
+#include <QSemaphore>
+#include <QSocketNotifier>
+#include <QString>
+#include <QThread>
+#include <QTimer>
+#include <QWidget>
+
+#include <clap/all.h>
+
+#include "engine.hh"
+#include "param-queue.hh"
+#include "plugin-param.hh"
+
+class Engine;
+class PluginHost final : public QObject {
+ Q_OBJECT;
+
+public:
+ PluginHost(Engine &engine);
+ ~PluginHost();
+
+ bool load(const QString &path, int pluginIndex);
+ void unload();
+
+ bool canActivate() const;
+ void activate(int32_t sample_rate);
+ void deactivate();
+
+ void setPorts(int numInputs, float **inputs, int numOutputs, float **outputs);
+ void setParentWindow(WId parentWindow);
+
+ void processInit(int nframes);
+ void processNoteOn(int sampleOffset, int channel, int key, int velocity);
+ void processNoteOff(int sampleOffset, int channel, int key, int velocity);
+ void processNoteAt(int sampleOffset, int channel, int key, int pressure);
+ void processPitchBend(int sampleOffset, int channel, int value);
+ void processCC(int sampleOffset, int channel, int cc, int value);
+ void process();
+
+ void idle();
+
+ void initPluginExtensions();
+ void initThreadPool();
+ void terminateThreadPool();
+ void threadPoolEntry();
+
+ void setParamValueByHost(PluginParam ¶m, clap_param_value value);
+
+ auto ¶ms() const { return params_; }
+
+ static void checkForMainThread();
+ static void checkForAudioThread();
+
+signals:
+ void paramsChanged();
+
+private:
+ static PluginHost *fromHost(clap_host *host);
+ template <typename T>
+ void initPluginExtension(const T *&ext, const char *id);
+
+ /* clap host callbacks */
+ static void clapHostLog(clap_host *host, clap_log_severity severity, const char *msg);
+
+ static bool clapIsMainThread(clap_host *host);
+ static bool clapIsAudioThread(clap_host *host);
+
+ static void clapParamsAdjustBegin(clap_host *host, clap_id param_id);
+ static void clapParamsAdjustEnd(clap_host *host, clap_id param_id);
+ static void clapParamsAdjust(clap_host *host, clap_id param_id, clap_param_value plain_value);
+ static void clapParamsRescan(clap_host *host, uint32_t flags);
+ void scanParams();
+ void scanParam(int32_t index);
+ PluginParam &checkValidParamId(const std::string_view &function,
+ const std::string_view ¶m_name,
+ clap_id param_id);
+ void checkValidParamValue(const PluginParam ¶m, clap_param_value value);
+ clap_param_value getParamValue(const clap_param_info &info);
+ static bool clapParamsRescanMayValueChange(uint32_t flags) {
+ return flags & (CLAP_PARAM_RESCAN_ALL | CLAP_PARAM_RESCAN_VALUES);
+ }
+ static bool clapParamsRescanMayInfoChange(uint32_t flags) {
+ return flags & (CLAP_PARAM_RESCAN_ALL | CLAP_PARAM_RESCAN_INFO);
+ }
+
+ static bool clapEventLoopRegisterTimer(clap_host *host, uint32_t period_ms, clap_id *timer_id);
+ static bool clapEventLoopUnregisterTimer(clap_host *host, clap_id timer_id);
+ static bool clapEventLoopRegisterFd(clap_host *host, clap_fd fd, uint32_t flags);
+ static bool clapEventLoopModifyFd(clap_host *host, clap_fd fd, uint32_t flags);
+ static bool clapEventLoopUnregisterFd(clap_host *host, clap_fd fd);
+ void eventLoopSetFdNotifierFlags(clap_fd fd, uint32_t flags);
+
+ static bool clapThreadPoolRequestExec(clap_host *host, uint32_t num_tasks);
+
+ static const void *clapHostExtension(clap_host *host, const char *extension);
+
+ /* clap host gui callbacks */
+ static bool clapHostGuiResize(clap_host *host, int32_t width, int32_t height);
+
+private:
+ Engine &engine_;
+
+ QLibrary library_;
+
+ clap_host host_;
+ clap_host_log hostLog_;
+ clap_host_gui hostGui_;
+ clap_host_audio_ports hostAudioPorts_;
+ clap_host_params hostParams_;
+ clap_host_event_loop hostEventLoop_;
+ clap_host_thread_check hostThreadCheck_;
+ clap_host_thread_pool hostThreadPool_;
+
+ const struct clap_plugin_entry * pluginEntry_ = nullptr;
+ clap_plugin * plugin_ = nullptr;
+ const clap_plugin_params * pluginParams_ = nullptr;
+ const clap_plugin_audio_ports * pluginAudioPorts_ = nullptr;
+ const clap_plugin_gui * pluginGui_ = nullptr;
+ const clap_plugin_gui_x11 * pluginGuiX11_ = nullptr;
+ const clap_plugin_gui_win32 * pluginGuiWin32_ = nullptr;
+ const clap_plugin_gui_cocoa * pluginGuiCocoa_ = nullptr;
+ const clap_plugin_gui_free_standing *pluginGuiFreeStanding_ = nullptr;
+ const clap_plugin_event_loop * pluginEventLoop_ = nullptr;
+ const clap_plugin_thread_pool * pluginThreadPool_ = nullptr;
+
+ bool pluginExtensionsAreInitialized_ = false;
+
+ /* timers */
+ clap_id nextTimerId_ = 0;
+ std::unordered_map<clap_id, std::unique_ptr<QTimer>> timers_;
+
+ /* fd events */
+ struct Notifiers {
+ std::unique_ptr<QSocketNotifier> rd;
+ std::unique_ptr<QSocketNotifier> wr;
+ std::unique_ptr<QSocketNotifier> err;
+ };
+ std::unordered_map<clap_fd, std::unique_ptr<Notifiers>> fds_;
+
+ /* thread pool */
+ std::vector<std::unique_ptr<QThread>> threadPool_;
+ std::atomic<bool> threadPoolStop_ = {false};
+ std::atomic<int> threadPoolTaskIndex_ = {0};
+ QSemaphore threadPoolSemaphoreProd_;
+ QSemaphore threadPoolSemaphoreDone_;
+
+ /* process stuff */
+ clap_audio_buffer audioIn_ = {};
+ clap_audio_buffer audioOut_ = {};
+ std::vector<clap_event> evIn_;
+ std::vector<clap_event> evOut_;
+ clap_process process_;
+
+ /* param update queues */
+ std::unordered_map<clap_id, std::unique_ptr<PluginParam>> params_;
+ ParamQueue appToEngineQueue_;
+ ParamQueue engineToAppQueue_;
+
+ /* delayed actions */
+ enum PluginState {
+ // The plugin is inactive, only the main thread uses it
+ Inactive,
+
+ // Activation failed
+ InactiveWithError,
+
+ // The plugin is active and sleeping, the audio engine can call set_processing()
+ ActiveAndSleeping,
+
+ // The plugin is processing
+ ActiveAndProcessing,
+
+ // The plugin did process but is in error
+ ActiveWithError,
+
+ // The plugin is not used anymore by the audio engine and can be deactivated on the main
+ // thread
+ ActiveAndReadyToDeactivate,
+ };
+
+ bool isPluginActive() const;
+ bool isPluginProcessing() const;
+ void setPluginState(PluginState state);
+
+ PluginState state_ = Inactive;
+ bool scheduleDeactivateForParameterScan_ = false;
+ uint32_t scheduleParamsRescanFlags_ = 0;
+};
diff --git a/examples/host/plugin-info.cc b/examples/host/plugin-info.cc
@@ -0,0 +1,3 @@
+#include "plugin-info.hh"
+
+PluginInfo::PluginInfo() {}
diff --git a/examples/host/plugin-info.hh b/examples/host/plugin-info.hh
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <QString>
+
+class PluginInfo {
+public:
+ PluginInfo();
+
+private:
+ QString name_;
+ QString file_;
+ QString index_; // in case of shell plugin
+};
diff --git a/examples/host/plugin-param.cc b/examples/host/plugin-param.cc
@@ -0,0 +1,118 @@
+#include "plugin-param.hh"
+#include "plugin-host.hh"
+
+PluginParam::PluginParam(PluginHost & pluginHost,
+ const clap_param_info &info,
+ clap_param_value value)
+ : QObject(&pluginHost), info_(info), value_(value), modulated_value_(value) {}
+
+void PluginParam::setValue(clap_param_value v) {
+ if (isValueEqualTo(v))
+ return;
+ value_ = v;
+ valueChanged();
+}
+
+void PluginParam::setModulatedValue(clap_param_value v) {
+ if (areValuesEqual(info_.type, modulated_value_, v))
+ return;
+ modulated_value_ = v;
+ modulatedValueChanged();
+}
+
+bool PluginParam::hasRange() const {
+ switch (info_.type) {
+ case CLAP_PARAM_INT:
+ case CLAP_PARAM_FLOAT:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool PluginParam::areValuesEqual(clap_param_type type, clap_param_value v1, clap_param_value v2) {
+ switch (type) {
+ case CLAP_PARAM_BOOL:
+ return v1.b == v2.b;
+ case CLAP_PARAM_ENUM:
+ case CLAP_PARAM_INT:
+ return v1.i == v2.i;
+ case CLAP_PARAM_FLOAT:
+ return v1.d == v2.d;
+ default:
+ std::terminate();
+ }
+}
+
+bool PluginParam::isValueEqualTo(const clap_param_value v) const {
+ return areValuesEqual(info_.type, value_, v);
+}
+
+bool PluginParam::isValueValid(const clap_param_value v) const {
+ switch (info_.type) {
+ case CLAP_PARAM_BOOL:
+ return true;
+ case CLAP_PARAM_ENUM:
+ return enum_entries_.find(v.i) != enum_entries_.end();
+ case CLAP_PARAM_INT:
+ return info_.min_value.i <= v.i && v.i <= info_.max_value.i;
+ case CLAP_PARAM_FLOAT:
+ return info_.min_value.d <= v.d && v.d <= info_.max_value.d;
+ default:
+ std::terminate();
+ }
+}
+
+void PluginParam::printInfo(std::ostream &os) const {
+ os << "id: " << info_.id << ", name: '" << info_.name << "', module: '" << info_.module << "'";
+
+ if (hasRange()) {
+ os << ", min: ";
+ printValue(info_.min_value, os);
+ os << ", max: ";
+ printValue(info_.max_value, os);
+ }
+}
+
+void PluginParam::printValue(const clap_param_value v, std::ostream &os) const {
+ switch (info_.type) {
+ case CLAP_PARAM_BOOL:
+ os << (v.b ? "true" : "false");
+ return;
+ case CLAP_PARAM_ENUM: {
+ auto it = enum_entries_.find(v.i);
+ if (it != enum_entries_.end())
+ os << it->second << "=" << v.i;
+ else
+ os << "(unknown enum entry)=" << v.i;
+ }
+ return;
+ case CLAP_PARAM_INT:
+ os << v.i;
+ return;
+ case CLAP_PARAM_FLOAT:
+ os << v.d;
+ return;
+ default:
+ std::terminate();
+ }
+}
+
+bool PluginParam::isInfoEqualTo(const clap_param_info &info) const {
+ return !isInfoCriticallyDifferentTo(info) &&
+ !strncmp(info_.name, info.name, sizeof(info.name)) &&
+ !strncmp(info_.module, info.module, sizeof(info.module)) &&
+ info_.is_used == info.is_used && info_.is_periodic == info.is_periodic &&
+ info_.is_hidden == info.is_hidden && info_.is_bypass == info.is_bypass &&
+ areValuesEqual(info.type, info_.default_value, info_.default_value);
+}
+
+bool PluginParam::isInfoCriticallyDifferentTo(const clap_param_info &info) const {
+ return info_.id != info.id || info_.is_per_note != info.is_per_note ||
+ info_.is_per_channel != info.is_per_channel || info_.is_locked != info.is_locked ||
+ info_.is_automatable != info.is_automatable || info_.type != info.type ||
+ ((info.type == CLAP_PARAM_INT || info.type == CLAP_PARAM_FLOAT) &&
+ !areValuesEqual(info.type, info_.min_value, info_.min_value) ||
+ !areValuesEqual(info.type, info_.max_value, info_.max_value)) ||
+ (info.type == CLAP_PARAM_ENUM && info.enum_entry_count != info_.enum_entry_count);
+}
+\ No newline at end of file
diff --git a/examples/host/plugin-param.hh b/examples/host/plugin-param.hh
@@ -0,0 +1,58 @@
+#pragma once
+
+#include <ostream>
+#include <unordered_map>
+
+#include <QObject>
+
+#include <clap/all.h>
+
+class PluginHost;
+class PluginParam : public QObject {
+ Q_OBJECT;
+
+public:
+ PluginParam(PluginHost &pluginHost, const clap_param_info &info, clap_param_value value);
+
+ clap_param_value value() const { return value_; }
+ void setValue(clap_param_value v);
+
+ clap_param_value modulatedValue() const { return modulated_value_; }
+ void setModulatedValue(clap_param_value v);
+
+ bool hasRange() const;
+ bool isValueEqualTo(const clap_param_value v) const;
+ bool isValueValid(const clap_param_value v) const;
+ static bool areValuesEqual(clap_param_type type, clap_param_value v1, clap_param_value v2);
+
+ void printInfo(std::ostream &os) const;
+ void printValue(const clap_param_value v, std::ostream &os) const;
+
+ void setInfo(const clap_param_info &info) noexcept { info_ = info; }
+ bool isInfoEqualTo(const clap_param_info &info) const;
+ bool isInfoCriticallyDifferentTo(const clap_param_info &info) const;
+ clap_param_info & info() noexcept { return info_; }
+ const clap_param_info &info() const noexcept { return info_; }
+
+ bool isBeingAdjusted() const noexcept { return is_being_adjusted_; }
+ void beginAdjust() {
+ Q_ASSERT(!is_being_adjusted_);
+ is_being_adjusted_ = true;
+ }
+ void endAdjust() {
+ Q_ASSERT(is_being_adjusted_);
+ is_being_adjusted_ = false;
+ }
+
+signals:
+ void infoChanged();
+ void valueChanged();
+ void modulatedValueChanged();
+
+private:
+ bool is_being_adjusted_ = false;
+ clap_param_info info_;
+ clap_param_value value_;
+ clap_param_value modulated_value_;
+ std::unordered_map<int64_t, std::string> enum_entries_;
+};
+\ No newline at end of file
diff --git a/examples/host/plugin-parameters-widget.cc b/examples/host/plugin-parameters-widget.cc
@@ -0,0 +1,326 @@
+#include <QFormLayout>
+#include <QFrame>
+#include <QLabel>
+#include <QLayout>
+#include <QSlider>
+#include <QSplitter>
+#include <QTreeWidget>
+
+#include "plugin-host.hh"
+#include "plugin-param.hh"
+#include "plugin-parameters-widget.hh"
+
+///////////////////
+// ParamTreeItem //
+///////////////////
+
+PluginParametersWidget::ParamTreeItem::ParamTreeItem(ModuleTreeItem *parent, PluginParam ¶m)
+ : QTreeWidgetItem(parent), param_(param) {}
+
+QVariant PluginParametersWidget::ParamTreeItem::data(int column, int role) const {
+ if (column == 0 && role == Qt::DisplayRole)
+ return param_.info().name;
+ return {};
+}
+
+void PluginParametersWidget::ParamTreeItem::setData(int column, int role, const QVariant &value) {
+ // nothing to do, read-only
+}
+
+////////////////////
+// ModuleTreeItem //
+////////////////////
+
+PluginParametersWidget::ModuleTreeItem::ModuleTreeItem(QTreeWidget *parent)
+ : QTreeWidgetItem(parent), name_("/") {}
+
+PluginParametersWidget::ModuleTreeItem::ModuleTreeItem(ModuleTreeItem *parent, const QString &name)
+ : QTreeWidgetItem(parent), name_(name) {}
+
+void PluginParametersWidget::ModuleTreeItem::clear() {
+ while (childCount() > 0)
+ removeChild(child(0));
+ modules_.clear();
+}
+
+PluginParametersWidget::ModuleTreeItem &
+PluginParametersWidget::ModuleTreeItem::subModule(const QString &name) {
+ auto it = modules_.find(name);
+ if (it != modules_.end())
+ return *it.value();
+ auto module = new ModuleTreeItem(this, name);
+ addChild(module);
+ modules_.insert(name, module);
+ return *module;
+}
+
+void PluginParametersWidget::ModuleTreeItem::addItem(ParamTreeItem *item) { addChild(item); }
+
+QVariant PluginParametersWidget::ModuleTreeItem::data(int column, int role) const {
+ if (column == 0 && role == Qt::DisplayRole)
+ return name_;
+ return {};
+}
+
+void PluginParametersWidget::ModuleTreeItem::setData(int column, int role, const QVariant &value) {
+ // nothing to do, read-only
+}
+
+////////////////////////////
+// PluginParametersWidget //
+////////////////////////////
+
+PluginParametersWidget::PluginParametersWidget(QWidget *parent, PluginHost &pluginHost)
+ : QWidget(parent), pluginHost_(pluginHost) {
+
+ treeWidget_ = new QTreeWidget(this);
+
+ // Tree
+ rootModuleItem_ = new ModuleTreeItem(treeWidget_);
+ treeWidget_->addTopLevelItem(rootModuleItem_);
+ treeWidget_->setHeaderHidden(true);
+ treeWidget_->setAnimated(true);
+ treeWidget_->setRootIsDecorated(true);
+ treeWidget_->setSelectionMode(QAbstractItemView::SingleSelection);
+ treeWidget_->setSelectionBehavior(QAbstractItemView::SelectItems);
+ treeWidget_->setVerticalScrollMode(QAbstractItemView::ScrollPerItem);
+ connect(
+ &pluginHost_, &PluginHost::paramsChanged, this, &PluginParametersWidget::computeDataModel);
+ connect(treeWidget_,
+ &QTreeWidget::currentItemChanged,
+ this,
+ &PluginParametersWidget::selectionChanged);
+
+ // Info
+ auto infoWidget = new QFrame(this);
+ infoWidget->setLineWidth(1);
+ infoWidget->setMidLineWidth(1);
+ infoWidget->setFrameShape(QFrame::StyledPanel);
+ infoWidget->setFrameShadow(QFrame::Sunken);
+
+ idLabel_ = new QLabel;
+ nameLabel_ = new QLabel;
+ moduleLabel_ = new QLabel;
+ isPerNoteLabel_ = new QLabel;
+ isPerChannelLabel_ = new QLabel;
+ isPeriodicLabel_ = new QLabel;
+ isLockedLabel_ = new QLabel;
+ isAutomatableLabel_ = new QLabel;
+ isHiddenLabel_ = new QLabel;
+ isBypassLabel_ = new QLabel;
+ typeLabel_ = new QLabel;
+ minValueLabel_ = new QLabel;
+ maxValueLabel_ = new QLabel;
+ defaultValueLabel_ = new QLabel;
+ isBeingAdjusted_ = new QLabel;
+ valueSlider_ = new QSlider;
+ valueSlider_->setMinimum(0);
+ valueSlider_->setMaximum(SLIDER_RANGE);
+ valueSlider_->setOrientation(Qt::Horizontal);
+ connect(valueSlider_, &QSlider::valueChanged, this, &PluginParametersWidget::sliderValueChanged);
+
+ auto formLayout = new QFormLayout(infoWidget);
+ formLayout->addRow(tr("id"), idLabel_);
+ formLayout->addRow(tr("name"), nameLabel_);
+ formLayout->addRow(tr("module"), moduleLabel_);
+ formLayout->addRow(tr("is_per_note"), isPerNoteLabel_);
+ formLayout->addRow(tr("is_per_channel"), isPerChannelLabel_);
+ formLayout->addRow(tr("is_periodic"), isPeriodicLabel_);
+ formLayout->addRow(tr("is_locked"), isLockedLabel_);
+ formLayout->addRow(tr("is_automatable"), isAutomatableLabel_);
+ formLayout->addRow(tr("is_hidden"), isHiddenLabel_);
+ formLayout->addRow(tr("is_bypass"), isBypassLabel_);
+ formLayout->addRow(tr("type"), typeLabel_);
+ formLayout->addRow(tr("min_value"), minValueLabel_);
+ formLayout->addRow(tr("max_value"), maxValueLabel_);
+ formLayout->addRow(tr("default_value"), defaultValueLabel_);
+ formLayout->addRow(tr("is_being_adjusted"), isBeingAdjusted_);
+ formLayout->addRow(tr("value"), valueSlider_);
+
+ infoWidget->setLayout(formLayout);
+
+ // Splitter
+ auto splitter = new QSplitter();
+ splitter->addWidget(treeWidget_);
+ splitter->addWidget(infoWidget);
+
+ auto layout = new QHBoxLayout(this);
+ layout->addWidget(splitter);
+ setLayout(layout);
+
+ computeDataModel();
+ updateParamInfo();
+}
+
+void PluginParametersWidget::computeDataModel() {
+ rootModuleItem_->clear();
+ idToParamTreeItem_.clear();
+
+ for (auto &it : pluginHost_.params()) {
+ auto ¶m = *it.second;
+
+ QString path(param.info().module);
+ auto modules = path.split("/", Qt::SkipEmptyParts);
+ auto module = rootModuleItem_;
+ for (auto &m : modules)
+ module = &module->subModule(m);
+
+ auto item = std::make_unique<ParamTreeItem>(module, param);
+ idToParamTreeItem_.emplace(param.info().id, std::move(item));
+ }
+ treeWidget_->sortItems(0, Qt::AscendingOrder);
+}
+
+void PluginParametersWidget::selectionChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous) {
+ if (!current) {
+ disconnectFromParam();
+ return;
+ }
+
+ auto module = dynamic_cast<ModuleTreeItem *>(current);
+ if (module) {
+ disconnectFromParam();
+ return;
+ }
+
+ auto item = dynamic_cast<ParamTreeItem *>(current);
+ if (item) {
+ connectToParam(&item->param());
+ return;
+ }
+}
+
+void PluginParametersWidget::connectToParam(PluginParam *param) {
+ if (currentParam_)
+ disconnectFromParam();
+
+ currentParam_ = param;
+ connect(param, &PluginParam::infoChanged, this, &PluginParametersWidget::paramInfoChanged);
+ connect(param, &PluginParam::valueChanged, this, &PluginParametersWidget::paramValueChanged);
+
+ updateParamInfo();
+ updateParamValue();
+}
+
+void PluginParametersWidget::disconnectFromParam() {
+ if (!currentParam_)
+ return;
+
+ disconnect(
+ currentParam_, &PluginParam::infoChanged, this, &PluginParametersWidget::paramInfoChanged);
+ disconnect(
+ currentParam_, &PluginParam::valueChanged, this, &PluginParametersWidget::paramValueChanged);
+ updateParamInfo();
+}
+
+void PluginParametersWidget::updateParamInfo() {
+ if (!currentParam_) {
+ idLabel_->setText("-");
+ nameLabel_->setText("-");
+ moduleLabel_->setText("-");
+ isPerNoteLabel_->setText("-");
+ isPerChannelLabel_->setText("-");
+ isPeriodicLabel_->setText("-");
+ isLockedLabel_->setText("-");
+ isAutomatableLabel_->setText("-");
+ isHiddenLabel_->setText("-");
+ isBypassLabel_->setText("-");
+ typeLabel_->setText("-");
+ minValueLabel_->setText("-");
+ maxValueLabel_->setText("-");
+ defaultValueLabel_->setText("-");
+ isBeingAdjusted_->setText("-");
+ } else {
+ auto &p = *currentParam_;
+ auto &i = p.info();
+ idLabel_->setText(QString::number(i.id));
+ nameLabel_->setText(i.name);
+ moduleLabel_->setText(i.module);
+ isPerNoteLabel_->setText(i.is_per_note ? "true" : "false");
+ isPerChannelLabel_->setText(i.is_per_channel ? "true" : "false");
+ isPeriodicLabel_->setText(i.is_periodic ? "true" : "false");
+ isLockedLabel_->setText(i.is_locked ? "true" : "false");
+ isAutomatableLabel_->setText(i.is_automatable ? "true" : "false");
+ isHiddenLabel_->setText(i.is_hidden ? "true" : "false");
+ isBypassLabel_->setText(i.is_bypass ? "true" : "false");
+ isBeingAdjusted_->setText(p.isBeingAdjusted() ? "true" : "false");
+
+ switch (i.type) {
+ case CLAP_PARAM_BOOL:
+ typeLabel_->setText("bool");
+ minValueLabel_->setText("-");
+ maxValueLabel_->setText("-");
+ defaultValueLabel_->setText(i.default_value.b ? "true" : "false");
+ break;
+
+ case CLAP_PARAM_INT:
+ typeLabel_->setText("int");
+ minValueLabel_->setText(QString::number(i.min_value.i));
+ maxValueLabel_->setText(QString::number(i.max_value.i));
+ defaultValueLabel_->setText(QString::number(i.default_value.i));
+ break;
+
+ case CLAP_PARAM_ENUM:
+ typeLabel_->setText("enum");
+ minValueLabel_->setText("-");
+ maxValueLabel_->setText("-");
+ defaultValueLabel_->setText(QString::number(i.default_value.i));
+ break;
+
+ case CLAP_PARAM_FLOAT:
+ typeLabel_->setText("float");
+ minValueLabel_->setText(QString::number(i.min_value.d));
+ maxValueLabel_->setText(QString::number(i.max_value.d));
+ defaultValueLabel_->setText(QString::number(i.default_value.d));
+ break;
+ }
+ }
+}
+
+void PluginParametersWidget::updateParamValue() {
+ if (valueSlider_->isSliderDown())
+ return;
+
+ if (!currentParam_)
+ return;
+
+ auto info = currentParam_->info();
+ auto v = currentParam_->value();
+ switch (info.type) {
+ case CLAP_PARAM_FLOAT:
+ valueSlider_->setValue(SLIDER_RANGE * (v.d - info.min_value.d) /
+ (info.max_value.d - info.min_value.d));
+ break;
+ case CLAP_PARAM_INT:
+ valueSlider_->setValue((SLIDER_RANGE * (v.i - info.min_value.i)) /
+ (info.max_value.i - info.min_value.i));
+ break;
+ }
+}
+
+void PluginParametersWidget::paramInfoChanged() { updateParamInfo(); }
+
+void PluginParametersWidget::paramValueChanged() { updateParamValue(); }
+
+void PluginParametersWidget::sliderValueChanged(int newValue) {
+ if (!currentParam_)
+ return;
+
+ if (!valueSlider_->isSliderDown())
+ return;
+
+ auto &info = currentParam_->info();
+
+ clap_param_value value;
+ switch (info.type) {
+ case CLAP_PARAM_FLOAT:
+ value.d = newValue * (info.max_value.d - info.min_value.d) / SLIDER_RANGE + info.min_value.d;
+ pluginHost_.setParamValueByHost(*currentParam_, value);
+ break;
+ case CLAP_PARAM_INT:
+ value.i =
+ (newValue * (info.max_value.i - info.min_value.i)) / SLIDER_RANGE + info.min_value.i;
+ pluginHost_.setParamValueByHost(*currentParam_, value);
+ break;
+ }
+}
+\ No newline at end of file
diff --git a/examples/host/plugin-parameters-widget.hh b/examples/host/plugin-parameters-widget.hh
@@ -0,0 +1,95 @@
+#pragma once
+
+#include <QHash>
+#include <QList>
+#include <QTreeWidgetItem>
+#include <QWidget>
+
+#include <clap/clap.h>
+
+class QTreeWidget;
+class QTreeWidgetItem;
+class PluginHost;
+class PluginParam;
+class QLabel;
+class QDial;
+class QSlider;
+
+class PluginParametersWidget : public QWidget {
+ Q_OBJECT
+public:
+ explicit PluginParametersWidget(QWidget *parent, PluginHost &pluginHost);
+
+ class ModuleTreeItem;
+ class ParamTreeItem : public QTreeWidgetItem {
+ public:
+ ParamTreeItem(ModuleTreeItem *parent, PluginParam ¶m);
+ QVariant data(int column, int role) const override;
+ void setData(int column, int role, const QVariant &value) override;
+
+ auto ¶m() { return param_; }
+ auto ¶m() const { return param_; }
+
+ private:
+ PluginParam ¶m_;
+ };
+
+ class ModuleTreeItem : public QTreeWidgetItem {
+ public:
+ ModuleTreeItem(QTreeWidget *parent);
+ ModuleTreeItem(ModuleTreeItem *parent, const QString &name);
+
+ void clear();
+
+ ModuleTreeItem &subModule(const QString &name);
+ void addItem(ParamTreeItem *item);
+
+ QVariant data(int column, int role) const override;
+ void setData(int column, int role, const QVariant &value) override;
+
+ private:
+ QString name_;
+ QHash<QString, ModuleTreeItem *> modules_;
+ };
+
+signals:
+
+private:
+ void computeDataModel();
+ void selectionChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous);
+
+ void connectToParam(PluginParam *param);
+ void disconnectFromParam();
+
+ void paramInfoChanged();
+ void paramValueChanged();
+ void sliderValueChanged(int newValue);
+
+ void updateParamInfo();
+ void updateParamValue();
+
+ static const constexpr int SLIDER_RANGE = 10000;
+
+ PluginHost & pluginHost_;
+ QTreeWidget * treeWidget_ = nullptr;
+ std::unordered_map<clap_id, std::unique_ptr<ParamTreeItem>> idToParamTreeItem_;
+ ModuleTreeItem * rootModuleItem_;
+ PluginParam * currentParam_ = nullptr;
+
+ QLabel * idLabel_ = nullptr;
+ QLabel * nameLabel_ = nullptr;
+ QLabel * moduleLabel_ = nullptr;
+ QLabel * isPerNoteLabel_ = nullptr;
+ QLabel * isPerChannelLabel_ = nullptr;
+ QLabel * isPeriodicLabel_ = nullptr;
+ QLabel * isLockedLabel_ = nullptr;
+ QLabel * isAutomatableLabel_ = nullptr;
+ QLabel * isHiddenLabel_ = nullptr;
+ QLabel * isBypassLabel_ = nullptr;
+ QLabel * typeLabel_ = nullptr;
+ QLabel * minValueLabel_ = nullptr;
+ QLabel * maxValueLabel_ = nullptr;
+ QLabel * defaultValueLabel_ = nullptr;
+ QLabel * isBeingAdjusted_ = nullptr;
+ QSlider *valueSlider_ = nullptr;
+};
diff --git a/examples/host/settings-dialog.cc b/examples/host/settings-dialog.cc
@@ -0,0 +1,27 @@
+#include <QDialogButtonBox>
+#include <QVBoxLayout>
+
+#include "settings-widget.hh"
+#include "settings.hh"
+
+#include "settings-dialog.hh"
+
+SettingsDialog::SettingsDialog(Settings &settings, QWidget *parent)
+ : QDialog(parent), settings_(settings) {
+ setModal(true);
+ setWindowTitle(tr("Settings"));
+
+ QVBoxLayout *vbox = new QVBoxLayout();
+ settingsWidget_ = new SettingsWidget(settings);
+ vbox->addWidget(settingsWidget_);
+
+ auto buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
+ buttons->show();
+ vbox->addWidget(buttons);
+ connect(buttons, SIGNAL(accepted()), this, SLOT(accept()));
+ connect(buttons, SIGNAL(rejected()), this, SLOT(reject()));
+
+ setLayout(vbox);
+
+ setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
+}
diff --git a/examples/host/settings-dialog.hh b/examples/host/settings-dialog.hh
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <QDialog>
+
+class Settings;
+class SettingsWidget;
+
+class SettingsDialog : public QDialog {
+public:
+ SettingsDialog(Settings &settings, QWidget *parent = nullptr);
+
+private:
+ Settings & settings_;
+ SettingsWidget *settingsWidget_ = nullptr;
+};
diff --git a/examples/host/settings-widget.cc b/examples/host/settings-widget.cc
@@ -0,0 +1,20 @@
+#include <QTabWidget>
+#include <QVBoxLayout>
+
+#include "audio-settings-widget.hh"
+#include "midi-settings-widget.hh"
+#include "settings-widget.hh"
+#include "settings.hh"
+
+SettingsWidget::SettingsWidget(Settings &settings) : settings_(settings) {
+ QVBoxLayout *layout = new QVBoxLayout();
+
+ audioSettingsWidget_ = new AudioSettingsWidget(settings.audioSettings());
+ layout->addWidget(audioSettingsWidget_);
+
+ midiSettingsWidget_ = new MidiSettingsWidget(settings.midiSettings());
+ layout->addWidget(midiSettingsWidget_);
+
+ setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
+ setLayout(layout);
+}
diff --git a/examples/host/settings-widget.hh b/examples/host/settings-widget.hh
@@ -0,0 +1,24 @@
+#pragma once
+
+#include <QWidget>
+
+class QTabWidget;
+class Settings;
+class AudioSettingsWidget;
+class MidiSettingsWidget;
+
+class SettingsWidget : public QWidget {
+ Q_OBJECT
+public:
+ explicit SettingsWidget(Settings &settings);
+
+signals:
+
+public slots:
+
+private:
+ QTabWidget * tabWidget_ = nullptr;
+ AudioSettingsWidget *audioSettingsWidget_ = nullptr;
+ MidiSettingsWidget * midiSettingsWidget_ = nullptr;
+ Settings & settings_;
+};
diff --git a/examples/host/settings.cc b/examples/host/settings.cc
@@ -0,0 +1,13 @@
+#include "settings.hh"
+
+Settings::Settings() {}
+
+void Settings::load(QSettings &settings) {
+ audioSettings_.load(settings);
+ midiSettings_.load(settings);
+}
+
+void Settings::save(QSettings &settings) const {
+ audioSettings_.save(settings);
+ midiSettings_.save(settings);
+}
diff --git a/examples/host/settings.hh b/examples/host/settings.hh
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "audio-settings.hh"
+#include "midi-settings.hh"
+
+class QSettings;
+
+class Settings {
+public:
+ Settings();
+
+ void load(QSettings &settings);
+ void save(QSettings &settings) const;
+
+ AudioSettings &audioSettings() { return audioSettings_; }
+ MidiSettings & midiSettings() { return midiSettings_; }
+
+private:
+ AudioSettings audioSettings_;
+ MidiSettings midiSettings_;
+};
diff --git a/include/clap/ext/draft/file-reference.h b/include/clap/ext/draft/file-reference.h
@@ -1,6 +1,7 @@
#pragma once
#include "../../clap.h"
+#include "../../hash.h"
#define CLAP_EXT_FILE_REFERENCE "clap/file-reference"
@@ -8,6 +9,19 @@
extern "C" {
#endif
+/// @page File Reference
+///
+/// This extension provides a way for the host to know about files which are used
+/// by the preset, like a wavetable, a sample, ...
+///
+/// The host can then:
+/// - collect and save
+/// - search for missing files by using:
+/// - filename
+/// - hash
+/// - be aware that some external file references are marked as dirty
+/// and needs to be saved.
+
typedef struct clap_file_reference {
clap_id resource_id;
char path[CLAP_PATH_SIZE];
@@ -23,15 +37,23 @@ typedef struct clap_plugin_file_reference {
// [main-thread]
bool (*get)(clap_plugin *plugin, uint32_t index, clap_file_reference *file_reference);
+ // [main-thread]
+ bool (*get_hash)(clap_plugin *plugin, clap_id resource_id, clap_hash hash, uint8_t *digest);
+
// updates the path to a file reference
// [main-thread]
bool (*set)(clap_plugin *plugin, clap_id resource_id, const char *path);
+
+ // [main-thread]
+ bool (*save_resources)(clap_plugin *plugin);
} clap_plugin_file_reference;
typedef struct clap_host_file_reference {
// informs the host that the file references have changed, the host should schedule a full rescan
// [main-thread]
void (*changed)(clap_host *host);
+
+ void (*set_dirty)(clap_host *host , clap_id resource_id);
} clap_host_file_reference;
#ifdef __cplusplus
diff --git a/include/clap/hash.h b/include/clap/hash.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "clap.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Commonly used hashing algorithms
+enum {
+ // 32 bits
+ CLAP_HASH_CRC32,
+
+ // 64 bits
+ CLAP_HASH_CRC64,
+
+ // 128 bits
+ CLAP_HASH_MD5,
+
+ // 160 bits
+ CLAP_HASH_SHA1,
+
+ // 256 bits
+ CLAP_HASH_SHA2,
+
+ // 512 bits
+ CLAP_HASH_SHA3,
+};
+typedef uint32_t clap_hash;
+
+#ifdef __cplusplus
+}
+#endif
+\ No newline at end of file