view flattendynamics-ladspa.cpp @ 13:98fd2543a5f2

Slightly more effective max rms
author Chris Cannam
date Tue, 22 Jul 2014 13:49:46 +0100
parents e54f4293614b
children 0e28664bc4fc
line wrap: on
line source
/* -*- c-basic-offset: 4 indent-tabs-mode: nil -*-  vi:set ts=8 sts=4 sw=4: */

#include "flattendynamics-ladspa.h"

#include <iostream>
#include <cmath>

using std::cerr;
using std::endl;

const float longTermSeconds = 4.f;
const float shortTermSeconds = 1.f;
const float catchUpSeconds = 0.2f;
const float targetMaxRMS = 0.04f;
const float rmsMaxDecay = 0.999f;
const float maxGain = 20.f;

const char *const
FlattenDynamics::portNames[PortCount] =
{
    "Input",
    "Output",
    "Gain",
};

const LADSPA_PortDescriptor 
FlattenDynamics::ports[PortCount] =
{
    LADSPA_PORT_INPUT  | LADSPA_PORT_AUDIO,
    LADSPA_PORT_OUTPUT | LADSPA_PORT_AUDIO,
    LADSPA_PORT_OUTPUT | LADSPA_PORT_CONTROL,
};

const LADSPA_PortRangeHint 
FlattenDynamics::hints[PortCount] =
{
    { 0, 0, 0 },
    { 0, 0, 0 },
    { 0, 0, 0 },
};

const LADSPA_Properties
FlattenDynamics::properties = LADSPA_PROPERTY_HARD_RT_CAPABLE;

const LADSPA_Descriptor 
FlattenDynamics::ladspaDescriptor =
{
    0xf0b375, // "Unique" ID
    "flattendynamics", // Label
    properties,
    "Flatten Dynamics", // Name
    "Queen Mary, University of London", // Maker
    "BSD", // Copyright
    PortCount,
    ports,
    portNames,
    hints,
    0, // Implementation data
    instantiate,
    connectPort,
    activate,
    run,
    0, // Run adding
    0, // Set run adding gain
    deactivate,
    cleanup
};

const LADSPA_Descriptor *
FlattenDynamics::getDescriptor(unsigned long index)
{
    if (index == 0) return &ladspaDescriptor;
    return 0;
}

FlattenDynamics::FlattenDynamics(int sampleRate) :
    m_sampleRate(sampleRate),
    m_input(0),
    m_output(0),
    m_pgain(0),
    m_history(0),
    m_histlen(0),
    m_histwrite(0),
    m_histread(0),
    m_sumOfSquaresLongTerm(0.f),
    m_sumOfSquaresShortTerm(0.f),
    m_rmsLongTerm(0.f),
    m_rmsShortTerm(0.f),
    m_maxRmsLongTerm(0.f),
    m_maxRmsShortTerm(0.f),
    m_gain(1.f)
{
    reset();
}

FlattenDynamics::~FlattenDynamics()
{
    delete[] m_history;
}
    
LADSPA_Handle
FlattenDynamics::instantiate(const LADSPA_Descriptor *, unsigned long rate)
{
    FlattenDynamics *flatten = new FlattenDynamics(rate);
    return flatten;
}

void
FlattenDynamics::connectPort(LADSPA_Handle handle,
                             unsigned long port, LADSPA_Data *location)
{
    FlattenDynamics *flatten = (FlattenDynamics *)handle;

    float **ports[PortCount] = {
	&flatten->m_input,
	&flatten->m_output,
        &flatten->m_pgain,
    };

    *ports[port] = (float *)location;
}

void
FlattenDynamics::activate(LADSPA_Handle handle)
{
    FlattenDynamics *flatten = (FlattenDynamics *)handle;
    flatten->reset();
}

void
FlattenDynamics::run(LADSPA_Handle handle, unsigned long samples)
{
    FlattenDynamics *flatten = (FlattenDynamics *)handle;
    flatten->runImpl(samples);
}

void
FlattenDynamics::deactivate(LADSPA_Handle handle)
{
    activate(handle); // both functions just reset the plugin
}

void
FlattenDynamics::cleanup(LADSPA_Handle handle)
{
    delete (FlattenDynamics *)handle;
}

void
FlattenDynamics::reset()
{
    delete[] m_history;
    m_histlen = int(round(m_sampleRate * longTermSeconds));
    if (m_histlen < 1) m_histlen = 1;
    m_history = new float[m_histlen];
    for (int i = 0; i < m_histlen; ++i) {
        m_history[i] = 0.f;
    }
    m_histwrite = 0;
    m_histread = 0;

    m_sumOfSquaresLongTerm = 0.0;
    m_sumOfSquaresShortTerm = 0.0;
    m_rmsLongTerm = 0.f;
    m_rmsShortTerm = 0.f;
    m_maxRmsLongTerm = 0.f;
    m_maxRmsShortTerm = 0.f;
    m_gain = 1.f;
}

void
FlattenDynamics::updateParameters()
{
    if (m_pgain) *m_pgain = m_gain;
}

void
FlattenDynamics::runImpl(unsigned long sampleCount)
{
    if (!m_input || !m_output) return;
    updateParameters();

    // Adjust gain so that
    // * RMS level of the past N seconds is some fixed R, but
    // * peak level does not clip
    // We aim to take M seconds to move to our target gain

    for (int i = 0; i < sampleCount; ++i) {
        m_output[i] = process(m_input[i]);
    }
}

float
FlattenDynamics::process(float f)
{
    updateRMS(f);

    if (m_rmsLongTerm == 0.f) {
        return f;
    }

    if (m_rmsLongTerm >= m_maxRmsLongTerm) {
        m_maxRmsLongTerm = m_rmsLongTerm;
    } else if (m_rmsLongTerm < m_maxRmsLongTerm * rmsMaxDecay) {
        m_maxRmsLongTerm *= rmsMaxDecay;
    }

    if (m_rmsShortTerm > m_maxRmsShortTerm) {
        m_maxRmsShortTerm = m_rmsShortTerm;
    }

    float fixedGain = targetMaxRMS / m_maxRmsLongTerm;

    float frac = m_rmsShortTerm / m_maxRmsShortTerm;

    // push up toward top of 0,1 range
    frac = pow(frac, 0.3);

    float targetRMS = (frac * m_maxRmsShortTerm);

    float targetGain = fixedGain * (targetRMS / m_rmsShortTerm);

    if (targetGain > maxGain) {
        targetGain = maxGain;
    }

    float catchUpSamples = catchUpSeconds * m_sampleRate;
    // asymptotic, could improve?
    m_gain = m_gain + (targetGain - m_gain) / catchUpSamples;

    if (fabsf(f) * m_gain > 1.f) {
        m_gain = 1.f / fabsf(f);
    }

//    cerr << "target gain = " << targetGain << ", gain = " << m_gain << endl;
    return f * m_gain;
}

void
FlattenDynamics::updateRMS(float f)
{
    // We update the RMS values by maintaining a sum-of-last-n-squares
    // total (which the RMS is the square root of 1/n of) and
    // recording the last n samples of history in a circular
    // buffer. When a sample drops off the start of the history, we
    // remove the square of it from the sum-of-squares total; then we
    // add the square of the new sample.

    int nextWrite = (m_histwrite + 1) % m_histlen;

    float loseLongTerm = 0.f;
    float loseShortTerm = 0.f;

    if (nextWrite == m_histread) {
        // full
        loseLongTerm = m_history[m_histread];
        m_histread = (m_histread + 1) % m_histlen;
    }

    int shortTermLength = round(shortTermSeconds * m_sampleRate);
    int shortTermLoseIndex = nextWrite - shortTermLength;
    if (shortTermLoseIndex < 0) {
        shortTermLoseIndex += m_histlen;
    }
    // This depends on history being zero-initialised, to be correct at start:
    loseShortTerm = m_history[shortTermLoseIndex];

    m_history[m_histwrite] = f;
    m_histwrite = nextWrite;

    int fill = (m_histwrite - m_histread + m_histlen) % m_histlen;

    m_sumOfSquaresLongTerm -= loseLongTerm * loseLongTerm;
    m_sumOfSquaresLongTerm += f * f;

    m_sumOfSquaresShortTerm -= loseShortTerm * loseShortTerm;
    m_sumOfSquaresShortTerm += f * f;

    m_rmsLongTerm = sqrt(m_sumOfSquaresLongTerm / fill);
    m_rmsShortTerm = sqrt(m_sumOfSquaresShortTerm / shortTermLength);
//    cerr << "rms = " << m_rms << " (from " << fill << " samples of " << m_histlen << ", latest " << f << ")" << endl;
}

const LADSPA_Descriptor *
ladspa_descriptor(unsigned long ix)
{
    return FlattenDynamics::getDescriptor(ix);
}