Files
lmms/plugins/LadspaEffect/LadspaEffect.cpp
Dalton Messmer 5916a0b477 Auto-quit rework (#8070)
This reworks the auto-quit feature by introducing a new AudioBuffer class which keeps track of which channels are currently silent as audio flows through the effects chain.

When track channels going into an effect's input are not marked as quiet, it is assumed a signal is present and the plugin needs to wake up if it is asleep due to auto-quit. After a plugin processes a buffer, the silence status is updated.

When the auto-quit setting is disabled (that is, when effects are always kept running), effects are always assumed to have input noise (a non-quiet signal present at the plugin inputs), which should result in the same behavior as before.

Benefits:

- The auto-quit system now closely follows how it is supposed to function by only waking plugins which have non-zero input rather than waking all plugins at once whenever an instrument plays a note or a sample track plays. This granularity better fits multi-channel plugins and pin connector routing where not all plugin inputs are connected to the same track channels. This means a sleeping plugin whose inputs are connected to channels 3/4 would not need to wake up if a signal is only present on channels 1/2.
- Silencing channels that are already known to be silent is a no-op
- Calculating the absolute peak sample value for a channel already known to be silent is a no-op
- The silence flags also could be useful for other purposes, such as adding visual indicators to represent how audio signals flow in and out of each plugin
- With a little more work, auto-quit could be enabled/disabled for plugins on an individual basis
- With a little more work, auto-quit could be implemented for instrument plugins
- AudioBuffer can be used with SharedMemory
- AudioBuffer could be used in plugins for their buffers

This new system works so long as the silence flags for each channel remain valid at each point along the effect chain. Modifying the buffers without an accompanying update of the silence flags could violate assumptions. Through unit tests, the correct functioning of AudioBuffer itself can be validated, but its usage in AudioBusHandle, Mixer, and a few other places where track channels are handled will need to be done with care.

---------

Co-authored-by: Sotonye Atemie <sakertooth@gmail.com>
2026-03-09 02:32:45 -04:00

548 lines
13 KiB
C++

/*
* LadspaEffect.cpp - class for processing LADSPA effects
*
* Copyright (c) 2006-2008 Danny McRae <khjklujn/at/users.sourceforge.net>
* Copyright (c) 2009-2014 Tobias Doerffel <tobydox/at/users.sourceforge.net>
*
* This file is part of LMMS - https://lmms.io
*
* 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 (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#include <QVarLengthArray>
#include <QMessageBox>
#include "LadspaEffect.h"
#include "DataFile.h"
#include "AudioEngine.h"
#include "Ladspa2LMMS.h"
#include "LadspaBase.h"
#include "LadspaControl.h"
#include "LadspaSubPluginFeatures.h"
#include "AutomationClip.h"
#include "ValueBuffer.h"
#include "Song.h"
#include "embed.h"
#include "plugin_export.h"
namespace lmms
{
extern "C"
{
Plugin::Descriptor PLUGIN_EXPORT ladspaeffect_plugin_descriptor =
{
LMMS_STRINGIFY( PLUGIN_NAME ),
"LADSPA",
QT_TRANSLATE_NOOP( "PluginBrowser",
"plugin for using arbitrary LADSPA-effects "
"inside LMMS." ),
"Danny McRae <khjklujn/at/users.sourceforge.net>",
0x0100,
Plugin::Type::Effect,
new PluginPixmapLoader("logo"),
nullptr,
new LadspaSubPluginFeatures( Plugin::Type::Effect )
} ;
}
LadspaEffect::LadspaEffect(Model* _parent, const Descriptor::SubPluginFeatures::Key* _key)
: Effect(&ladspaeffect_plugin_descriptor, _parent, _key)
, m_controls(nullptr)
, m_key(LadspaSubPluginFeatures::subPluginKeyToLadspaKey(_key))
{
Ladspa2LMMS * manager = Engine::getLADSPAManager();
if( manager->getDescription( m_key ) == nullptr )
{
Engine::getSong()->collectError(tr( "Unknown LADSPA plugin %1 requested." ).arg(
m_key.second ) );
setOkay( false );
return;
}
setDisplayName( manager->getShortName( m_key ) );
pluginInstantiation();
connect( Engine::audioEngine(), SIGNAL( sampleRateChanged() ),
this, SLOT( changeSampleRate() ) );
}
LadspaEffect::~LadspaEffect()
{
pluginDestruction();
}
void LadspaEffect::changeSampleRate()
{
DataFile dataFile( DataFile::Type::EffectSettings );
m_controls->saveState( dataFile, dataFile.content() );
LadspaControls * controls = m_controls;
m_controls = nullptr;
m_pluginMutex.lock();
pluginDestruction();
pluginInstantiation();
m_pluginMutex.unlock();
controls->effectModelChanged( m_controls );
delete controls;
m_controls->restoreState( dataFile.content().firstChild().toElement() );
// the IDs of re-created controls have been saved and now need to be
// resolved again
AutomationClip::resolveAllIDs();
}
Effect::ProcessStatus LadspaEffect::processImpl(SampleFrame* buf, const fpp_t frames)
{
m_pluginMutex.lock();
if (!isProcessingAudio())
{
m_pluginMutex.unlock();
return ProcessStatus::Sleep;
}
// Copy the LMMS audio buffer to the LADSPA input buffer and initialize
// the control ports.
ch_cnt_t channel = 0;
for( ch_cnt_t proc = 0; proc < processorCount(); ++proc )
{
for( int port = 0; port < m_portCount; ++port )
{
port_desc_t * pp = m_ports.at( proc ).at( port );
switch( pp->rate )
{
case BufferRate::ChannelIn:
for (fpp_t frame = 0; frame < frames; ++frame)
{
pp->buffer[frame] = buf[frame][channel];
}
++channel;
break;
case BufferRate::AudioRateInput:
{
ValueBuffer * vb = pp->control->valueBuffer();
if( vb )
{
memcpy(pp->buffer, vb->values(), frames * sizeof(float));
}
else
{
pp->value = static_cast<LADSPA_Data>(
pp->control->value() / pp->scale );
// This only supports control rate ports, so the audio rates are
// treated as though they were control rate by setting the
// port buffer to all the same value.
for (fpp_t frame = 0; frame < frames; ++frame)
{
pp->buffer[frame] = pp->value;
}
}
break;
}
case BufferRate::ControlRateInput:
if( pp->control == nullptr )
{
break;
}
pp->value = static_cast<LADSPA_Data>(
pp->control->value() / pp->scale );
pp->buffer[0] =
pp->value;
break;
case BufferRate::ChannelOut:
case BufferRate::AudioRateOutput:
case BufferRate::ControlRateOutput:
break;
default:
break;
}
}
}
// Process the buffers.
for( ch_cnt_t proc = 0; proc < processorCount(); ++proc )
{
(m_descriptor->run)(m_handles[proc], frames);
}
// Copy the LADSPA output buffers to the LMMS buffer.
channel = 0;
const float d = dryLevel();
const float w = wetLevel();
for( ch_cnt_t proc = 0; proc < processorCount(); ++proc )
{
for( int port = 0; port < m_portCount; ++port )
{
port_desc_t * pp = m_ports.at( proc ).at( port );
switch( pp->rate )
{
case BufferRate::ChannelIn:
case BufferRate::AudioRateInput:
case BufferRate::ControlRateInput:
break;
case BufferRate::ChannelOut:
for (fpp_t frame = 0; frame < frames; ++frame)
{
buf[frame][channel] = d * buf[frame][channel] + w * pp->buffer[frame];
}
++channel;
break;
case BufferRate::AudioRateOutput:
case BufferRate::ControlRateOutput:
break;
default:
break;
}
}
}
m_pluginMutex.unlock();
return ProcessStatus::ContinueIfNotQuiet;
}
void LadspaEffect::setControl( int _control, LADSPA_Data _value )
{
if( !isOkay() )
{
return;
}
m_portControls[_control]->value = _value;
}
void LadspaEffect::pluginInstantiation()
{
Ladspa2LMMS * manager = Engine::getLADSPAManager();
// Calculate how many processing units are needed.
int effect_channels = manager->getDescription( m_key )->inputChannels;
m_processors = DEFAULT_CHANNELS / effect_channels;
// get inPlaceBroken property
m_inPlaceBroken = manager->isInplaceBroken( m_key );
// Categorize the ports, and create the buffers.
m_portCount = manager->getPortCount( m_key );
int inputch = 0;
int outputch = 0;
std::array<LADSPA_Data*, 2> inbuf;
inbuf[0] = nullptr;
inbuf[1] = nullptr;
for( ch_cnt_t proc = 0; proc < processorCount(); proc++ )
{
multi_proc_t ports;
for( int port = 0; port < m_portCount; port++ )
{
auto p = new port_desc_t;
p->name = manager->getPortName( m_key, port );
p->proc = proc;
p->port_id = port;
p->control = nullptr;
p->buffer = nullptr;
// Determine the port's category.
if( manager->isPortAudio( m_key, port ) )
{
if( p->name.toUpper().contains( "IN" ) &&
manager->isPortInput( m_key, port ) )
{
p->rate = BufferRate::ChannelIn;
p->buffer = new LADSPA_Data[Engine::audioEngine()->framesPerPeriod()];
inbuf[ inputch ] = p->buffer;
inputch++;
}
else if( p->name.toUpper().contains( "OUT" ) &&
manager->isPortOutput( m_key, port ) )
{
p->rate = BufferRate::ChannelOut;
if( ! m_inPlaceBroken && inbuf[ outputch ] )
{
p->buffer = inbuf[ outputch ];
outputch++;
}
else
{
p->buffer = new LADSPA_Data[Engine::audioEngine()->framesPerPeriod()];
m_inPlaceBroken = true;
}
}
else if( manager->isPortInput( m_key, port ) )
{
p->rate = BufferRate::AudioRateInput;
p->buffer = new LADSPA_Data[Engine::audioEngine()->framesPerPeriod()];
}
else
{
p->rate = BufferRate::AudioRateOutput;
p->buffer = new LADSPA_Data[Engine::audioEngine()->framesPerPeriod()];
}
}
else
{
p->buffer = new LADSPA_Data[1];
if( manager->isPortInput( m_key, port ) )
{
p->rate = BufferRate::ControlRateInput;
}
else
{
p->rate = BufferRate::ControlRateOutput;
}
}
p->scale = 1.0f;
if( manager->isEnum( m_key, port ) )
{
p->data_type = BufferDataType::Enum;
}
else if( manager->isPortToggled( m_key, port ) )
{
p->data_type = BufferDataType::Toggled;
}
else if( manager->isInteger( m_key, port ) )
{
p->data_type = BufferDataType::Integer;
}
else if( p->name.toUpper().contains( "(SECONDS)" ) )
{
p->data_type = BufferDataType::Time;
p->scale = 1000.0f;
int loc = p->name.toUpper().indexOf(
"(SECONDS)" );
p->name.replace( loc, 9, "(ms)" );
}
else if( p->name.toUpper().contains( "(S)" ) )
{
p->data_type = BufferDataType::Time;
p->scale = 1000.0f;
int loc = p->name.toUpper().indexOf( "(S)" );
p->name.replace( loc, 3, "(ms)" );
}
else if( p->name.toUpper().contains( "(MS)" ) )
{
p->data_type = BufferDataType::Time;
int loc = p->name.toUpper().indexOf( "(MS)" );
p->name.replace( loc, 4, "(ms)" );
}
else
{
p->data_type = BufferDataType::Floating;
}
// Get the range and default values.
p->max = manager->getUpperBound( m_key, port );
if( p->max == NOHINT )
{
p->max = p->name.toUpper() == "GAIN" ? 10.0f :
1.0f;
}
if( manager->areHintsSampleRateDependent(
m_key, port ) )
{
p->max *= Engine::audioEngine()->outputSampleRate();
}
p->min = manager->getLowerBound( m_key, port );
if( p->min == NOHINT )
{
p->min = 0.0f;
}
if( manager->areHintsSampleRateDependent(
m_key, port ) )
{
p->min *= Engine::audioEngine()->outputSampleRate();
}
p->def = manager->getDefaultSetting( m_key, port );
if( p->def == NOHINT )
{
if( p->data_type != BufferDataType::Toggled )
{
p->def = ( p->min + p->max ) / 2.0f;
}
else
{
p->def = 1.0f;
}
}
else if( manager->areHintsSampleRateDependent( m_key, port ) )
{
p->def *= Engine::audioEngine()->outputSampleRate();
}
p->max *= p->scale;
p->min *= p->scale;
p->def *= p->scale;
p->value = p->def;
p->suggests_logscale = manager->isLogarithmic( m_key, port );
ports.append( p );
// For convenience, keep a separate list of the ports that are used
// to control the processors.
if( p->rate == BufferRate::AudioRateInput ||
p->rate == BufferRate::ControlRateInput )
{
p->control_id = m_portControls.count();
m_portControls.append( p );
}
}
m_ports.append( ports );
}
// Instantiate the processing units.
m_descriptor = manager->getDescriptor( m_key );
if( m_descriptor == nullptr )
{
QMessageBox::warning( 0, "Effect",
"Can't get LADSPA descriptor function: " + m_key.second,
QMessageBox::Ok, QMessageBox::NoButton );
setOkay( false );
return;
}
if( m_descriptor->run == nullptr )
{
QMessageBox::warning( 0, "Effect",
"Plugin has no processor: " + m_key.second,
QMessageBox::Ok, QMessageBox::NoButton );
setDontRun( true );
}
for( ch_cnt_t proc = 0; proc < processorCount(); proc++ )
{
LADSPA_Handle effect = manager->instantiate(m_key, Engine::audioEngine()->outputSampleRate());
if( effect == nullptr )
{
QMessageBox::warning( 0, "Effect",
"Can't get LADSPA instance: " + m_key.second,
QMessageBox::Ok, QMessageBox::NoButton );
setOkay( false );
return;
}
m_handles.append( effect );
}
// Connect the ports.
for( ch_cnt_t proc = 0; proc < processorCount(); proc++ )
{
for( int port = 0; port < m_portCount; port++ )
{
port_desc_t * pp = m_ports.at( proc ).at( port );
if( !manager->connectPort( m_key,
m_handles[proc],
port,
pp->buffer ) )
{
QMessageBox::warning( 0, "Effect",
"Failed to connect port: " + m_key.second,
QMessageBox::Ok, QMessageBox::NoButton );
setDontRun( true );
return;
}
}
}
// Activate the processing units.
for( ch_cnt_t proc = 0; proc < processorCount(); proc++ )
{
manager->activate( m_key, m_handles[proc] );
}
m_controls = new LadspaControls( this );
}
void LadspaEffect::pluginDestruction()
{
if( !isOkay() )
{
return;
}
delete m_controls;
for( ch_cnt_t proc = 0; proc < processorCount(); proc++ )
{
Ladspa2LMMS * manager = Engine::getLADSPAManager();
manager->deactivate( m_key, m_handles[proc] );
manager->cleanup( m_key, m_handles[proc] );
for( int port = 0; port < m_portCount; port++ )
{
port_desc_t * pp = m_ports.at( proc ).at( port );
if( m_inPlaceBroken || pp->rate != BufferRate::ChannelOut )
{
if( pp->buffer) delete[] pp->buffer;
}
delete pp;
}
m_ports[proc].clear();
}
m_ports.clear();
m_handles.clear();
m_portControls.clear();
}
extern "C"
{
// necessary for getting instance out of shared lib
PLUGIN_EXPORT Plugin * lmms_plugin_main( Model * _parent, void * _data )
{
return new LadspaEffect( _parent,
static_cast<const Plugin::Descriptor::SubPluginFeatures::Key *>(
_data ) );
}
}
} // namespace lmms