clap

CLAP Audio Plugin API
Log | Files | Refs | README | LICENSE

commit 75ca319211e73a70656a757d8508fd5f7ff58bc8
parent 684637631358bf4dcdad8abf61d97f6f170b2037
Author: Alexandre BIQUE <bique.alexandre@gmail.com>
Date:   Tue, 18 May 2021 22:49:39 +0200

Add minimal host

Diffstat:
MCMakeLists.txt | 8++++++--
Aexamples/CMakeLists.txt | 4++++
Aexamples/host/.clang-format | 137+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/host/CMakeLists.txt | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/host/application.cc | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/host/application.hh | 46++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/host/audio-settings-widget.cc | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/host/audio-settings-widget.hh | 24++++++++++++++++++++++++
Aexamples/host/audio-settings.cc | 24++++++++++++++++++++++++
Aexamples/host/audio-settings.hh | 27+++++++++++++++++++++++++++
Aexamples/host/device-reference.cc | 1+
Aexamples/host/device-reference.hh | 8++++++++
Aexamples/host/engine.cc | 248+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/host/engine.hh | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/host/main-window.cc | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/host/main-window.hh | 33+++++++++++++++++++++++++++++++++
Aexamples/host/main.cc | 41+++++++++++++++++++++++++++++++++++++++++
Aexamples/host/midi-settings-widget.cc | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/host/midi-settings-widget.hh | 21+++++++++++++++++++++
Aexamples/host/midi-settings.cc | 20++++++++++++++++++++
Aexamples/host/midi-settings.hh | 19+++++++++++++++++++
Aexamples/host/param-queue.cc | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/host/param-queue.hh | 30++++++++++++++++++++++++++++++
Aexamples/host/plugin-host.cc | 939+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/host/plugin-host.hh | 196+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/host/plugin-info.cc | 3+++
Aexamples/host/plugin-info.hh | 13+++++++++++++
Aexamples/host/plugin-param.cc | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/host/plugin-param.hh | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/host/plugin-parameters-widget.cc | 327+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/host/plugin-parameters-widget.hh | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aexamples/host/settings-dialog.cc | 27+++++++++++++++++++++++++++
Aexamples/host/settings-dialog.hh | 15+++++++++++++++
Aexamples/host/settings-widget.cc | 20++++++++++++++++++++
Aexamples/host/settings-widget.hh | 24++++++++++++++++++++++++
Aexamples/host/settings.cc | 13+++++++++++++
Aexamples/host/settings.hh | 21+++++++++++++++++++++
Minclude/clap/ext/draft/file-reference.h | 22++++++++++++++++++++++
Ainclude/clap/hash.h | 34++++++++++++++++++++++++++++++++++
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 ? &params : nullptr, + &params, + 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 &param_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 &param, 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 &param = 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 &param = 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 &param = 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 &param, 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 &param, clap_param_value value); + + auto &params() 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 &param_name, + clap_id param_id); + void checkValidParamValue(const PluginParam &param, 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 &param) + : 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 &param = *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 &param); + QVariant data(int column, int role) const override; + void setData(int column, int role, const QVariant &value) override; + + auto &param() { return param_; } + auto &param() const { return param_; } + + private: + PluginParam &param_; + }; + + 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