obs-studio/plugins/obs-vst/VSTPlugin.cpp
2023-01-28 23:07:48 -08:00

468 lines
11 KiB
C++

/*****************************************************************************
Copyright (C) 2016-2017 by Colin Edwards.
Additional Code Copyright (C) 2016-2017 by c3r1c3 <c3r1c3@nevermindonline.com>
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 2 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 <http://www.gnu.org/licenses/>.
*****************************************************************************/
#include "headers/VSTPlugin.h"
#include <util/platform.h>
intptr_t VSTPlugin::hostCallback_static(AEffect *effect, int32_t opcode,
int32_t index, intptr_t value,
void *ptr, float opt)
{
UNUSED_PARAMETER(opt);
VSTPlugin *plugin = nullptr;
if (effect && effect->user) {
plugin = static_cast<VSTPlugin *>(effect->user);
}
switch (opcode) {
case audioMasterVersion:
return (intptr_t)2400;
case audioMasterGetCurrentProcessLevel:
return 1;
// We always replace, never accumulate
case audioMasterWillReplaceOrAccumulate:
return 1;
case audioMasterGetSampleRate:
if (plugin) {
return (intptr_t)plugin->GetSampleRate();
}
return 0;
case audioMasterGetVendorString:
strncpy((char *)ptr, "OBS Studio", 11);
return 1;
case audioMasterGetTime:
if (plugin) {
return (intptr_t)plugin->GetTimeInfo();
}
return 0;
// index: width, value: height
case audioMasterSizeWindow:
if (plugin && plugin->editorWidget) {
plugin->editorWidget->handleResizeRequest(index, value);
}
return 1;
default:
return 0;
}
}
VstTimeInfo *VSTPlugin::GetTimeInfo()
{
mTimeInfo.nanoSeconds = os_gettime_ns() / 1000000;
return &mTimeInfo;
}
float VSTPlugin::GetSampleRate()
{
return mTimeInfo.sampleRate;
}
VSTPlugin::VSTPlugin(obs_source_t *sourceContext) : sourceContext{sourceContext}
{
}
VSTPlugin::~VSTPlugin()
{
unloadEffect();
cleanupChannelBuffers();
}
void VSTPlugin::createChannelBuffers(size_t count)
{
cleanupChannelBuffers();
int blocksize = BLOCK_SIZE;
numChannels = (std::max)((size_t)0, count);
if (numChannels > 0) {
inputs = (float **)bmalloc(sizeof(float *) * numChannels);
outputs = (float **)bmalloc(sizeof(float *) * numChannels);
channelrefs = (float **)bmalloc(sizeof(float *) * numChannels);
for (size_t channel = 0; channel < numChannels; channel++) {
inputs[channel] =
(float *)bmalloc(sizeof(float) * blocksize);
outputs[channel] =
(float *)bmalloc(sizeof(float) * blocksize);
}
}
}
void VSTPlugin::cleanupChannelBuffers()
{
for (size_t channel = 0; channel < numChannels; channel++) {
if (inputs && inputs[channel]) {
bfree(inputs[channel]);
inputs[channel] = NULL;
}
if (outputs && outputs[channel]) {
bfree(outputs[channel]);
outputs[channel] = NULL;
}
}
if (inputs) {
bfree(inputs);
inputs = NULL;
}
if (outputs) {
bfree(outputs);
outputs = NULL;
}
if (channelrefs) {
bfree(channelrefs);
channelrefs = NULL;
}
numChannels = 0;
}
void VSTPlugin::loadEffectFromPath(const std::string &path)
{
if (this->pluginPath.compare(path) != 0) {
unloadEffect();
blog(LOG_INFO, "User selected new VST plugin: '%s'",
path.c_str());
}
if (!effect) {
// TODO: alert user of error if VST is not available.
pluginPath = path;
AEffect *effectTemp = loadEffect();
if (!effectTemp) {
blog(LOG_WARNING, "VST Plug-in: Can't load effect!");
return;
}
{
std::lock_guard<std::recursive_mutex> lock(lockEffect);
effect = effectTemp;
}
// Check plug-in's magic number
// If incorrect, then the file either was not loaded properly,
// is not a real VST plug-in, or is otherwise corrupt.
if (effect->magic != kEffectMagic) {
blog(LOG_WARNING, "VST Plug-in's magic number is bad");
return;
}
int maxchans =
(std::max)(effect->numInputs, effect->numOutputs);
// sanity check
if (maxchans < 0 || maxchans > 256) {
blog(LOG_WARNING,
"VST Plug-in has invalid number of channels");
return;
}
createChannelBuffers(maxchans);
// It is better to invoke this code after checking magic number
effect->dispatcher(effect, effGetEffectName, 0, 0, effectName,
0);
effect->dispatcher(effect, effGetVendorString, 0, 0,
vendorString, 0);
// This check logic is refer to open source project : Audacity
if ((effect->flags & effFlagsIsSynth) ||
!(effect->flags & effFlagsCanReplacing)) {
blog(LOG_WARNING,
"VST Plug-in can't support replacing. '%s'",
path.c_str());
return;
}
// Ask the plugin to identify itself...might be needed for older plugins
effect->dispatcher(effect, effIdentify, 0, 0, nullptr, 0.0f);
effect->dispatcher(effect, effOpen, 0, 0, nullptr, 0.0f);
// Set some default properties
size_t sampleRate =
audio_output_get_sample_rate(obs_get_audio());
// Initialize time info
memset(&mTimeInfo, 0, sizeof(mTimeInfo));
mTimeInfo.sampleRate = sampleRate;
mTimeInfo.nanoSeconds = os_gettime_ns() / 1000000;
mTimeInfo.tempo = 120.0;
mTimeInfo.timeSigNumerator = 4;
mTimeInfo.timeSigDenominator = 4;
mTimeInfo.flags = kVstTempoValid | kVstNanosValid |
kVstTransportPlaying;
effect->dispatcher(effect, effSetSampleRate, 0, 0, nullptr,
sampleRate);
int blocksize = BLOCK_SIZE;
effect->dispatcher(effect, effSetBlockSize, 0, blocksize,
nullptr, 0.0f);
effect->dispatcher(effect, effMainsChanged, 0, 1, nullptr, 0);
effectReady = true;
if (openInterfaceWhenActive) {
openEditor();
}
}
}
static void silenceChannel(float **channelData, size_t numChannels,
long numFrames)
{
for (size_t channel = 0; channel < numChannels; ++channel) {
for (long frame = 0; frame < numFrames; ++frame) {
channelData[channel][frame] = 0.0f;
}
}
}
obs_audio_data *VSTPlugin::process(struct obs_audio_data *audio)
{
// Here we check the status firstly,
// which help avoid waiting for lock while unloadEffect() is running.
bool effectValid = (effect && effectReady && numChannels > 0);
if (!effectValid)
return audio;
std::lock_guard<std::recursive_mutex> lock(lockEffect);
if (effect && effectReady && numChannels > 0) {
uint passes = (audio->frames + BLOCK_SIZE - 1) / BLOCK_SIZE;
uint extra = audio->frames % BLOCK_SIZE;
for (uint pass = 0; pass < passes; pass++) {
uint frames = pass == passes - 1 && extra ? extra
: BLOCK_SIZE;
silenceChannel(outputs, numChannels, BLOCK_SIZE);
for (size_t d = 0; d < numChannels; d++) {
if (d < MAX_AV_PLANES &&
audio->data[d] != nullptr) {
channelrefs[d] =
((float *)audio->data[d]) +
(pass * BLOCK_SIZE);
} else {
channelrefs[d] = inputs[d];
}
};
effect->processReplacing(effect, channelrefs, outputs,
frames);
// only copy back the channels the plugin may have generated
for (size_t c = 0; c < (size_t)effect->numOutputs &&
c < MAX_AV_PLANES;
c++) {
if (audio->data[c]) {
for (size_t i = 0; i < frames; i++) {
channelrefs[c][i] =
outputs[c][i];
}
}
}
}
}
return audio;
}
void VSTPlugin::unloadEffect()
{
closeEditor();
{
std::lock_guard<std::recursive_mutex> lock(lockEffect);
// Reset the status firstly to avoid VSTPlugin::process is blocked
effectReady = false;
if (effect) {
effect->dispatcher(effect, effMainsChanged, 0, 0,
nullptr, 0);
effect->dispatcher(effect, effClose, 0, 0, nullptr,
0.0f);
}
effect = nullptr;
}
unloadLibrary();
pluginPath.clear();
}
bool VSTPlugin::isEditorOpen()
{
return editorWidget ? true : false;
}
void VSTPlugin::onEditorClosed()
{
if (!editorWidget)
return;
editorWidget->deleteLater();
editorWidget = nullptr;
if (effect && editorOpened) {
editorOpened = false;
effect->dispatcher(effect, effEditClose, 0, 0, nullptr, 0);
}
}
void VSTPlugin::openEditor()
{
if (effect && !editorWidget) {
// This check logic is refer to open source project : Audacity
if (!(effect->flags & effFlagsHasEditor)) {
blog(LOG_WARNING,
"VST Plug-in: Can't support edit feature. '%s'",
pluginPath.c_str());
return;
}
editorOpened = true;
editorWidget = new EditorWidget(nullptr, this);
editorWidget->buildEffectContainer(effect);
if (sourceName.empty()) {
sourceName = "VST 2.x";
}
if (filterName.empty()) {
editorWidget->setWindowTitle(QString("%1 - %2").arg(
sourceName.c_str(), effectName));
} else {
editorWidget->setWindowTitle(
QString("%1: %2 - %3")
.arg(sourceName.c_str(),
filterName.c_str(), effectName));
}
editorWidget->show();
}
}
void VSTPlugin::closeEditor()
{
if (editorWidget)
editorWidget->close();
}
std::string VSTPlugin::getEffectPath()
{
return pluginPath;
}
std::string VSTPlugin::getChunk()
{
if (!effect) {
return "";
}
if (effect->flags & effFlagsProgramChunks) {
void *buf = nullptr;
intptr_t chunkSize = effect->dispatcher(effect, effGetChunk, 1,
0, &buf, 0.0);
QByteArray data = QByteArray((char *)buf, chunkSize);
return QString(data.toBase64()).toStdString();
} else {
std::vector<float> params;
for (int i = 0; i < effect->numParams; i++) {
float parameter = effect->getParameter(effect, i);
params.push_back(parameter);
}
const char *bytes = reinterpret_cast<const char *>(&params[0]);
QByteArray data =
QByteArray(bytes, (int)(sizeof(float) * params.size()));
std::string encoded = QString(data.toBase64()).toStdString();
return encoded;
}
}
void VSTPlugin::setChunk(const std::string &data)
{
if (!effect) {
return;
}
if (effect->flags & effFlagsProgramChunks) {
QByteArray base64Data =
QByteArray(data.c_str(), (int)data.length());
QByteArray chunkData = QByteArray::fromBase64(base64Data);
void *buf = nullptr;
buf = chunkData.data();
effect->dispatcher(effect, effSetChunk, 1, chunkData.length(),
buf, 0);
} else {
QByteArray base64Data =
QByteArray(data.c_str(), (int)data.length());
QByteArray paramData = QByteArray::fromBase64(base64Data);
const char *p_chars = paramData.data();
const float *p_floats =
reinterpret_cast<const float *>(p_chars);
const size_t size = paramData.length() / sizeof(float);
std::vector<float> params(p_floats, p_floats + size);
if (params.size() != (size_t)effect->numParams) {
return;
}
for (int i = 0; i < effect->numParams; i++) {
effect->setParameter(effect, i, params[i]);
}
}
}
void VSTPlugin::setProgram(const int programNumber)
{
if (programNumber < effect->numPrograms) {
effect->dispatcher(effect, effSetProgram, 0, programNumber,
NULL, 0.0f);
} else {
blog(LOG_ERROR,
"Failed to load program, number was outside possible program range.");
}
}
int VSTPlugin::getProgram()
{
return effect->dispatcher(effect, effGetProgram, 0, 0, NULL, 0.0f);
}
void VSTPlugin::getSourceNames()
{
/* Only call inside the vst_filter_audio function! */
sourceName = obs_source_get_name(obs_filter_get_parent(sourceContext));
filterName = obs_source_get_name(sourceContext);
}