Yes. That is correct.
The topology goes like this:
- Chains are sequences of units.
- Units are directed acyclic graphs (DAG) of objects.
- Objects have a defined set of named input and output ports as well as named parameters/options. Objects also define an algorithm for computing the outputs given the inputs and parameters/options.
Here is what the Limiter object looks like:
/*
* Limiter.h
*
* Created on: 22 Jun 2016
* Author: clarkson
*/
#ifndef APP_OBJECTS_ARITH_LIMITER_H_
#define APP_OBJECTS_ARITH_LIMITER_H_
#include <app/objects/Object.h>
namespace er301 {
class Limiter: public Object {
public:
Limiter(const std::string & name);
virtual ~Limiter();
#ifndef SWIGLUA
virtual void process();
Inlet mInput { "In" };
Outlet mOutput { "Out" };
Option mType { "Type", LimiterChoices::cubic };
#endif
};
} /* namespace er301 */
#endif /* APP_OBJECTS_ARITH_LIMITER_H_ */
/*
* Limiter.cpp
*
* Created on: 22 Jun 2016
* Author: clarkson
*/
#include <app/objects/arith/Limiter.h>
#include <arm_neon.h>
namespace er301 {
Limiter::Limiter(const std::string & name) :
Object(name) {
addInput(mInput);
addOutput(mOutput);
addOption(mType);
}
Limiter::~Limiter() {
}
static inline void tanhLimiting(float * in, float * out, float * end) {
#if 0
// tanh approximation (regular floating point)
// https://varietyofsound.wordpress.com/2011/02/14/efficient-tanh-computation-using-lamberts-continued-fraction/
// 55k ticks
while (out < end) {
float x = *in;
float x2 = x * x;
float a = (((x2 + 378) * x2 + 17325) * x2 + 135135) * x;
float b = ((28 * x2 + 3150) * x2 + 62370) * x2 + 135135;
*out = a / b;
in++;
out++;
}
#elif 1
// tanh approximation (neon w/ division via newton's method)
// https://varietyofsound.wordpress.com/2011/02/14/efficient-tanh-computation-using-lamberts-continued-fraction/
// 10k ticks
float32x4_t c1 = vdupq_n_f32(378);
float32x4_t c2 = vdupq_n_f32(17325);
float32x4_t c3 = vdupq_n_f32(135135);
float32x4_t c4 = vdupq_n_f32(28);
float32x4_t c5 = vdupq_n_f32(3150);
float32x4_t c6 = vdupq_n_f32(62370);
float32x4_t x, x2, a, b, invb;
while (out < end) {
x = vld1q_f32(in);
x2 = x * x;
a = (((x2 + c1) * x2 + c2) * x2 + c3) * x;
b = ((c4 * x2 + c5) * x2 + c6) * x2 + c3;
// https://en.wikipedia.org/wiki/Division_algorithm#Newton.E2.80.93Raphson_division
invb = vrecpeq_f32(b);
// iterate 3 times for 24 bits of precision
invb = vmulq_f32(invb,vrecpsq_f32(b,invb));
invb = vmulq_f32(invb,vrecpsq_f32(b,invb));
invb = vmulq_f32(invb,vrecpsq_f32(b,invb));
vst1q_f32(out, a * invb);
out += 4;
in += 4;
}
#elif 0
// tanh approximation (neon w/ regular division)
// https://varietyofsound.wordpress.com/2011/02/14/efficient-tanh-computation-using-lamberts-continued-fraction/
// 28k ticks
float32x4_t c1 = vdupq_n_f32(378);
float32x4_t c2 = vdupq_n_f32(17325);
float32x4_t c3 = vdupq_n_f32(135135);
float32x4_t c4 = vdupq_n_f32(28);
float32x4_t c5 = vdupq_n_f32(3150);
float32x4_t c6 = vdupq_n_f32(62370);
float32x4_t x, x2, a, b;
while (out < end) {
x = vld1q_f32(in);
x2 = x * x;
a = (((x2 + c1) * x2 + c2) * x2 + c3) * x;
b = ((c4 * x2 + c5) * x2 + c6) * x2 + c3;
vst1q_f32(out, a / b);
out += 4;
in += 4;
}
#endif
}
static inline void invSqrtLimiting(float * in, float * out, float * end) {
// x/sqrt(x*x + 1) (neon w/ inv sqrt via newton's method)
// 7k ticks
float32x4_t one = vdupq_n_f32(1.0f);
float32x4_t x, b, invb;
while (out < end) {
x = vld1q_f32(in);
b = x * x + one;
invb = vrsqrteq_f32(b); // 4k ticks
// iterate 3 times for 24 bits of precision
invb = invb * vrsqrtsq_f32(b, invb * invb); // 5k ticks
invb = invb * vrsqrtsq_f32(b, invb * invb); // 6k ticks
invb = invb * vrsqrtsq_f32(b, invb * invb); // 7k ticks
vst1q_f32(out, x * invb);
out += 4;
in += 4;
}
}
static inline void cubicLimiting(float * in, float * out, float * end) {
// x - x*x*x/6
// 2200 ticks
float32x4_t c = vdupq_n_f32(-1.0f/6.0f);
float32x4_t x, x3;
float32x4_t max = vdupq_n_f32(1.4142135623730951f);
float32x4_t min = vdupq_n_f32(-1.4142135623730951f);
while (out < end) {
x = vld1q_f32(in);
x = vminq_f32(x, max);
x = vmaxq_f32(x, min);
x3 = vmulq_f32(x,vmulq_f32(x,x));
vst1q_f32(out, vmlaq_f32(x,c,x3));
out += 4;
in += 4;
}
}
static inline void hardLimiting(float * in, float * out, float * end) {
float32x4_t max = vdupq_n_f32(1.0);
float32x4_t min = vdupq_n_f32(-1.0);
float32x4_t x;
while (out < end) {
x = vld1q_f32(in);
x = vminq_f32(x, max);
x = vmaxq_f32(x, min);
vst1q_f32(out, x);
out += 4;
in += 4;
}
}
void Limiter::process() {
float * in = mInput.buffer();
float * out = mOutput.buffer();
float * end = out + global.frameLength;
switch (mType.value()) {
case LimiterChoices::invSqrt:
invSqrtLimiting(in, out, end);
break;
case LimiterChoices::cubic:
cubicLimiting(in, out, end);
break;
case LimiterChoices::hard:
hardLimiting(in, out, end);
break;
}
}
}
/* namespace er301 */
Up to here is all C++. Lua is where objects are instantiated, connected in to DAGs inside a unit. Then the UI is also described in Lua at the unit level. Like this and take particular notice of the onLoadGraph and onLoadInterface methods:
-- LimiterUnit.lua
-- GLOBALS: app, os, verboseLevel, connect, tie
local app = app
local Class = require "Class"
local UnitSection = require "Chains.UnitSection"
local Fader = require "UnitControls.Fader"
local ModeSelect = require "UnitControls.ModeSelect"
local Encoder = require "Encoder"
local ply = app.SECTION_PLY
local LimiterUnit = Class{}
LimiterUnit:include(UnitSection)
function LimiterUnit:init(args)
args.title = "Limiter"
args.mnemonic = "LR"
UnitSection.init(self,args)
end
-- creation/destruction states
function LimiterUnit:onLoadGraph(pUnit, channelCount)
if channelCount==2 then
self:loadStereoGraph(pUnit)
else
self:loadMonoGraph(pUnit)
end
self.objects.inGain:hardSet("Gain",1.0)
self.objects.outGain:hardSet("Gain",1.0)
end
function LimiterUnit:loadMonoGraph(pUnit)
-- create objects
local inGain = self:createObject("ConstantGain","inGain")
local outGain = self:createObject("ConstantGain","outGain")
local limiter = self:createObject("Limiter","limiter")
-- connect inputs/outputs
connect(pUnit,"In1",inGain,"In")
connect(inGain,"Out",limiter,"In")
connect(limiter,"Out",outGain,"In")
connect(outGain,"Out",pUnit,"Out1")
end
function LimiterUnit:loadStereoGraph(pUnit)
-- create objects
local inGain1 = self:createObject("ConstantGain","inGain1")
local outGain1 = self:createObject("ConstantGain","outGain1")
local limiter1 = self:createObject("Limiter","limiter1")
-- connect inputs/outputs
connect(pUnit,"In1",inGain1,"In")
connect(inGain1,"Out",limiter1,"In")
connect(limiter1,"Out",outGain1,"In")
connect(outGain1,"Out",pUnit,"Out1")
-- create objects
local inGain2 = self:createObject("ConstantGain","inGain2")
local outGain2 = self:createObject("ConstantGain","outGain2")
local limiter2 = self:createObject("Limiter","limiter2")
-- connect inputs/outputs
connect(pUnit,"In2",inGain2,"In")
connect(inGain2,"Out",limiter2,"In")
connect(limiter2,"Out",outGain2,"In")
connect(outGain2,"Out",pUnit,"Out2")
tie(inGain2,"Gain",inGain1,"Gain")
self.objects.inGain = inGain1
tie(outGain2,"Gain",outGain1,"Gain")
self.objects.outGain = outGain1
tie(limiter2,"Type",limiter1,"Type")
self.objects.limiter = limiter1
end
local views = {
menu = {"rename","load","save"},
expanded = {"pre","type","post"},
collapsed = {},
}
function LimiterUnit:onLoadInterface(objects,controls)
controls.pre = Fader {
button = "pre",
description = "Pre-Gain",
param = objects.inGain:getParameter("Gain"),
monitor = self,
map = Encoder.getMap("decibel36"),
units = app.unitDecibels
}
controls.post = Fader {
button = "post",
description = "Post-Gain",
param = objects.outGain:getParameter("Gain"),
monitor = self,
map = Encoder.getMap("decibel36"),
units = app.unitDecibels
}
controls.type = ModeSelect {
button = "type",
description = "Type",
option = objects.limiter:getOption("Type"),
choices = {"inv sqrt","cubic","hard"}
}
if self.channelCount==1 then
local outlet = objects.inGain:getOutput("Out")
controls.pre:setMonoMeterTarget(outlet)
else
local left = objects.inGain1:getOutput("Out")
local right = objects.inGain2:getOutput("Out")
controls.pre:setStereoMeterTarget(left,right)
end
if self.channelCount==1 then
local outlet = objects.outGain:getOutput("Out")
controls.post:setMonoMeterTarget(outlet)
else
local left = objects.outGain1:getOutput("Out")
local right = objects.outGain2:getOutput("Out")
controls.post:setStereoMeterTarget(left,right)
end
return views
end
return LimiterUnit
Simple, right?