/*** Olive - Non-Linear Video Editor Copyright (C) 2019 Olive Team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ***/ #include "vsthost.h" // adapted from http://teragonaudio.com/article/How-to-make-your-own-VST-host.html #include #include #include #include #include #include #include "rendering/audio.h" #include "ui/mainwindow.h" #include "global/global.h" #include "global/debug.h" // Load libraries for retrieving the native window handle. Used for VST plugins that have a separate window // dedicated to controls. #if defined(Q_OS_WIN) #include #elif defined(Q_OS_MACOS) #include class NSWindow; #elif defined(Q_OS_LINUX) #include #endif #define BLOCK_SIZE 512 struct VSTRect { int16_t top; int16_t left; int16_t bottom; int16_t right; }; #define effGetChunk 23 #define effSetChunk 24 // C callbacks extern "C" { // Main host callback intptr_t hostCallback(AEffect* effect, int32_t opcode, int32_t index, intptr_t value, void* ptr, float opt) { Q_UNUSED(value) switch(opcode) { case audioMasterAutomate: effect->setParameter(effect, index, opt); break; case audioMasterVersion: return 2400; case audioMasterIdle: effect->dispatcher(effect, effEditIdle, 0, 0, nullptr, 0); break; case audioMasterWantMidi: // no midi support, return 0 break; case audioMasterGetSampleRate: return current_audio_freq(); case audioMasterGetBlockSize: return BLOCK_SIZE; case audioMasterGetCurrentProcessLevel: // process level happens to be 0 break; case audioMasterGetProductString: strcpy(static_cast(ptr), "OLIVETEAM"); break; case audioMasterBeginEdit: // we don't really care about this // but we are aware of it break; case audioMasterEndEdit: // change made olive::Global->set_modified(true); break; default: qInfo() << "Plugin requested unhandled opcode" << opcode; } return 0; } } // Plugin's entry point typedef AEffect *(*vstPluginFuncPtr)(audioMasterCallback host); // Plugin's getParameter() method typedef float (*getParameterFuncPtr)(AEffect *effect, int32_t index); // Plugin's setParameter() method typedef void (*setParameterFuncPtr)(AEffect *effect, int32_t index, float value); // Plugin's processEvents() method typedef int32_t (*processEventsFuncPtr)(VstEvents *events); // Plugin's process() method typedef void (*processFuncPtr)(AEffect *effect, float **inputs, float **outputs, int32_t sampleFrames); void VSTHost::loadPlugin() { QString dll_fn = file_field->GetFileAt(0); if (dll_fn.isEmpty()) { return; } // Try to load the plugin modulePtr.setFileName(dll_fn); if (!modulePtr.load()) { // Show an error if the plugin fails to load qCritical() << "Failed to load VST plugin" << dll_fn << "-" << modulePtr.errorString(); QMessageBox::critical(olive::MainWindow, tr("Error loading VST plugin"), tr("Failed to load VST plugin \"%1\": %2").arg(dll_fn, modulePtr.errorString())); return; } // Try to find the VST entry point (first using VSTPluginMain() ) vstPluginFuncPtr mainEntryPoint = reinterpret_cast(modulePtr.resolve("VSTPluginMain")); if (mainEntryPoint == nullptr) { // If there's no VSTPluginMain(), the plugin may use main() instead mainEntryPoint = reinterpret_cast(modulePtr.resolve("main")); } if (mainEntryPoint == nullptr) { QMessageBox::critical(olive::MainWindow, tr("Error loading VST plugin"), tr("Failed to locate entry point for dynamic library.")); modulePtr.unload(); return; } // Instantiate the plugin plugin = mainEntryPoint(hostCallback); } void VSTHost::freePlugin() { if (plugin != nullptr) { stopPlugin(); data_cache.clear(); modulePtr.unload(); plugin = nullptr; } } bool VSTHost::configurePluginCallbacks() { // Check plugin's magic number // If incorrect, then the file either was not loaded properly, is not a // real VST plugin, or is otherwise corrupt. if(plugin->magic != kEffectMagic) { qCritical() << "Plugin's magic number is bad"; QMessageBox::critical(olive::MainWindow, tr("VST Error"), tr("Plugin's magic number is invalid")); return false; } // Create dispatcher handle dispatcher = reinterpret_cast(plugin->dispatcher); // Set up plugin callback functions plugin->getParameter = reinterpret_cast(plugin->getParameter); plugin->processReplacing = reinterpret_cast(plugin->processReplacing); plugin->setParameter = reinterpret_cast(plugin->setParameter); return true; } void VSTHost::startPlugin() { dispatcher(plugin, effOpen, 0, 0, nullptr, 0.0f); // Set some default properties dispatcher(plugin, effSetSampleRate, 0, 0, nullptr, current_audio_freq()); dispatcher(plugin, effSetBlockSize, 0, BLOCK_SIZE, nullptr, 0.0f); resumePlugin(); } void VSTHost::stopPlugin() { suspendPlugin(); dispatcher(plugin, effClose, 0, 0, nullptr, 0); } void VSTHost::resumePlugin() { dispatcher(plugin, effMainsChanged, 0, 1, nullptr, 0.0f); } void VSTHost::suspendPlugin() { dispatcher(plugin, effMainsChanged, 0, 0, nullptr, 0.0f); } bool VSTHost::canPluginDo(char *canDoString) { return (dispatcher(plugin, effCanDo, 0, 0, static_cast(canDoString), 0.0f) > 0); } void VSTHost::CreateDialogIfNull() { if (dialog == nullptr) { dialog = new QDialog(olive::MainWindow); dialog->setWindowTitle(tr("VST Plugin")); dialog->setAttribute(Qt::WA_NativeWindow, true); dialog->setWindowFlags(dialog->windowFlags() | Qt::MSWindowsFixedSizeDialogHint); connect(dialog, SIGNAL(finished(int)), this, SLOT(uncheck_show_button())); } } void VSTHost::send_data_cache_to_plugin() { dispatcher(plugin, effSetChunk, 0, int32_t(data_cache.size()), static_cast(data_cache.data()), 0); } VSTHost::VSTHost(Clip* c, const EffectMeta *em) : Effect(c, em), plugin(nullptr), dialog(nullptr), input_cache(BLOCK_SIZE), output_cache(BLOCK_SIZE) { plugin = nullptr; EffectRow* file_row = new EffectRow(this, tr("Plugin"), true, false); file_field = new FileField(file_row, "filename"); connect(file_field, SIGNAL(Changed()), this, SLOT(change_plugin()), Qt::QueuedConnection); EffectRow* interface_row = new EffectRow(this, tr("Interface"), false, false); show_interface_btn = new ButtonField(interface_row, tr("Show")); show_interface_btn->SetCheckable(true); show_interface_btn->SetEnabled(false); connect(show_interface_btn, SIGNAL(Toggled(bool)), this, SLOT(show_interface(bool))); } VSTHost::~VSTHost() { freePlugin(); } void VSTHost::process_audio(double timecode_start, double timecode_end, float **samples, int nb_samples, int channel_count, int type) { if (plugin != nullptr) { // Make copy of audio input_cache.Create(channel_count); output_cache.Create(channel_count); for (int i=0;iprocessReplacing(plugin, input_cache.data(), output_cache.data(), sample_size); // Copy output cache back to samples for (int j=0;j 0) { stream.writeTextElement("plugindata", data_cache.toBase64()); } } void VSTHost::show_interface(bool show) { CreateDialogIfNull(); dialog->setVisible(show); if (show) { #if defined(Q_OS_WIN) dispatcher(plugin, effEditOpen, 0, 0, reinterpret_cast(dialog->windowHandle()->winId()), 0); #elif defined(Q_OS_MACOS) dispatcher(plugin, effEditOpen, 0, 0, reinterpret_cast(dialog->windowHandle()->winId()), 0); #elif defined(Q_OS_LINUX) || defined(__HAIKU__) dispatcher(plugin, effEditOpen, 0, 0, reinterpret_cast(dialog->windowHandle()->winId()), 0); #endif } else { dispatcher(plugin, effEditClose, 0, 0, nullptr, 0); } } void VSTHost::uncheck_show_button() { show_interface_btn->SetChecked(false); } void VSTHost::change_plugin() { freePlugin(); loadPlugin(); if (plugin != nullptr) { if (configurePluginCallbacks()) { startPlugin(); VSTRect* eRect = nullptr; plugin->dispatcher(plugin, effEditGetRect, 0, 0, &eRect, 0); if (!data_cache.isEmpty()) { send_data_cache_to_plugin(); } CreateDialogIfNull(); dialog->setFixedSize(eRect->right - eRect->left, eRect->bottom - eRect->top); } else { modulePtr.unload(); plugin = nullptr; } } show_interface_btn->SetEnabled(plugin != nullptr); } SampleCache::SampleCache(int block_size) : block_size_(block_size), channel_count_(0), array_(nullptr) { } SampleCache::~SampleCache() { destroy(); } void SampleCache::Create(int channels) { if (channel_count_ != channels) { if (channel_count_ > 0) { destroy(); } channel_count_ = channels; array_ = new float* [channel_count_]; for (int i=0;i