a7c3d94e4c
Please visit the submodule repo for blame history: https://github.com/obsproject/obs-vst This also replaces the obs-vst .clang-format with obs-studio's. This commit depends on the previous commit, which removes the submodule separately as Git complains otherwise. Co-authored-by: Alex Anderson <anderson.john.alexander@gmail.com> Co-authored-by: Anton <camotank12345@gmail.com> Co-authored-by: Blue Cat Audio <support@bluecataudio.com> Co-authored-by: Cephas Reis <c3r1c3@nevermindonline.com> Co-authored-by: Colin Edwards <colin@recursivepenguin.com> Co-authored-by: Florian Zwoch <fzwoch@gmail.com> Co-authored-by: Fogmoon <i@fogmoon.com> Co-authored-by: Gol-D-Ace <Gol-D-Ace@users.noreply.github.com> Co-authored-by: Igor Bochkariov <ujifgc@gmail.com> Co-authored-by: Jesse Chappell <jesse@sonosaurus.com> Co-authored-by: Keen <523059842@qq.com> Co-authored-by: Kurt Kartaltepe <kkartaltepe@gmail.com> Co-authored-by: Matt Gajownik <matt@obsproject.com> Co-authored-by: Matt Gajownik <matt@wizardcm.com> Co-authored-by: Richard Stanway <notr1ch@users.noreply.github.com> Co-authored-by: Ryan Foster <RytoEX@gmail.com> Co-authored-by: follower <github@rancidbacon.com> Co-authored-by: gxalpha <beckmann.sebastian@outlook.de> Co-authored-by: jp9000 <obs.jim@gmail.com> Co-authored-by: jpark37 <jpark37@users.noreply.github.com> Co-authored-by: mntone <sd8@live.jp> Co-authored-by: tytan652 <tytan652@tytanium.xyz> Co-authored-by: wangshaohui <97082645@qq.com> Co-authored-by: wangshaohui <wang.shaohui@navercorp.com>
465 lines
11 KiB
C++
465 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);
|
|
UNUSED_PARAMETER(ptr);
|
|
|
|
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 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 **)malloc(sizeof(float *) * numChannels);
|
|
outputs = (float **)malloc(sizeof(float *) * numChannels);
|
|
channelrefs = (float **)malloc(sizeof(float *) * numChannels);
|
|
for (size_t channel = 0; channel < numChannels; channel++) {
|
|
inputs[channel] =
|
|
(float *)malloc(sizeof(float) * blocksize);
|
|
outputs[channel] =
|
|
(float *)malloc(sizeof(float) * blocksize);
|
|
}
|
|
}
|
|
}
|
|
|
|
void VSTPlugin::cleanupChannelBuffers()
|
|
{
|
|
for (size_t channel = 0; channel < numChannels; channel++) {
|
|
if (inputs && inputs[channel]) {
|
|
free(inputs[channel]);
|
|
inputs[channel] = NULL;
|
|
}
|
|
if (outputs && outputs[channel]) {
|
|
free(outputs[channel]);
|
|
outputs[channel] = NULL;
|
|
}
|
|
}
|
|
if (inputs) {
|
|
free(inputs);
|
|
inputs = NULL;
|
|
}
|
|
if (outputs) {
|
|
free(outputs);
|
|
outputs = NULL;
|
|
}
|
|
if (channelrefs) {
|
|
free(channelrefs);
|
|
channelrefs = NULL;
|
|
}
|
|
numChannels = 0;
|
|
}
|
|
|
|
void VSTPlugin::loadEffectFromPath(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 = "";
|
|
}
|
|
|
|
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 *>(¶ms[0]);
|
|
QByteArray data =
|
|
QByteArray(bytes, (int)(sizeof(float) * params.size()));
|
|
std::string encoded = QString(data.toBase64()).toStdString();
|
|
return encoded;
|
|
}
|
|
}
|
|
|
|
void VSTPlugin::setChunk(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);
|
|
}
|