Recreating stereo enhancer


(ffx) #1

Hi, I am currently trying to rebuild the stereo enhancer effect in lua. Now it turns out that the “surround” effect isn’t just a simple stereo rotator, it does more than that, but I don’t know what it is :smile: Any idea maybe? Thanks.

Maybe it LFO modulates the 90 degree signal with the inverted LFO on the -90 degree signal? Or 0° with 90°?


#2

Curious how you would do this in lua? Are you looking at offline processing?

Anyway on a slight tangent, I found that the Stereo Enhancer is not mono compatible so I made a doofer out of native EQ’s which works fine in mono as well… It was in the downloads section in the old forum but I can’t find that here so I have attached it: Mono 2 Stereo 1.1.xrdp (36.6 KB)


(ffx) #3

@afta8 thank you! Though I am looking for a replacement vst for usage in Bitwig conversions, so I am using the LUA Protoplug VST and already created a very similar plugin now, maybe even more useful due using a variable allpass filter.

The stereo enhancer is useful for me esp. on material which originally wasn’t mono compatible, so the logic is like: not mono compatible + not mono compatible = mono compatible :grin:

Still that surround algorithm… I really think now it leaks in the middle signal into the side and vice versa, done by purpose, in a modulated way. That’s why I once thought it was buggy and reported it as a bug, assuming it should have been a simple stereo rotation, but in fact, it isn’t that simple.


(Zer0 Fly) #4

Maybe something like this:

http://www.musicdsp.org/en/latest/Effects/255-stereo-field-rotation-via-transformation-matrix.html


(ffx) #5

Ah yes, exactly this one I added, but it is a “usual” stereo rotator, but the surround algorithm has some additional “magic” going on.


(The Empty Self) #6

im using the multiband FX with the tool loaded so i can play with the width …its that what you want to achieve or you like the character of the renoise effect ? :slight_smile:


#7

sorry to interrupt - is that chain or native multiband fx device? :slight_smile:


(The Empty Self) #8

is native in bitwig i think @ffx is moving his projects to Bitiwig


#9

sorry, i overlooked that! i’m aware, i was excited to see something similar in renoise, so i had to jump in. :stuck_out_tongue:

Thanks again!


(The Empty Self) #10

np bro :slight_smile:


#11

since it’s bitwig talk, i love the way processing works, how you can add pre to (for example) , post/pre fx for reverb, for distortion only wet fx, or so… it’s opening a lot of ways!


(ffx) #12

Hi, yes, it is a VST. Yes, I want to recreate the character of that device, for super accurate conversion :badteeth:


(ffx) #13

Here is the code of the current version, currently lacking of “surround” and parameter interpolation, so do not automate it. Also this may be what a serious audio engineer would consider as harmful. But actually you could use it to make a synth phasing by purpose:

-- ffx Stereo Expander
--
-- missing: surround
-- with additional basic stereo rotator

require "include/protoplug"
local cbFilter = require "include/dsp/cookbook filters"

local width = 1
local phase = 0
local sur = 0
local apmix = 0
local monofreq = 0
local filter
local filterMono
local filterMono2
local swaplr = "off"
local lSample, rSample, panning, panning0
 
function stereoWiden(LSample, RSample, myWidth)
    local mono = (LSample+RSample) * 0.5
    local stereo = LSample - RSample
    stereo = stereo * myWidth
    return mono+stereo, mono-stereo
end
function phaseRotate(in_left, in_right)
    local phasepi = phase * math.pi
    out_left   = (in_left *  math.cos(phasepi))  - (in_right * math.sin(phasepi + math.pi));
    out_right = (in_left * -math.sin(phasepi)) - (in_right * math.cos(phasepi + math.pi));
    return out_left, out_right
end
function surround(in_left, in_right)
    out_left   = in_left;
    out_right = out_right;
    return out_left, out_right
end

stereoFx.init()

function stereoFx.Channel:init()
	-- create per-channel fields (filters)
	filter = cbFilter
	{
		-- initialize filters with current param values
		type 	= "ap";
		f 		= params[5].getValue()/2;  -- 10 ... 20000
		gain 	= 0;  -- -30 ... 30
		Q 		= params[6].getValue(); -- 0.1 ... 30
	}
	filterMono = cbFilter
	{
		-- initialize filters with current param values
		type 	= "lp";
		f 		= params[9].getValue()/2;  -- 10 ... 20000
		gain 	= 0;  -- -30 ... 30
		Q 		= 0.2; -- 0.1 ... 30
	}
	filterMono2 = cbFilter
	{
		-- initialize filters with current param values
		type 	= "lp";
		f 		= params[9].getValue()/2;  -- 10 ... 20000
		gain 	= 0;  -- -30 ... 30
		Q 		= 0.2; -- 0.1 ... 30
	}
end


function plugin.processBlock(samples, smax)
    for i = 0, smax do
        samples[0][i], samples[1][i] = 
            samples[0][i] * (1-panning0),
            samples[1][i] * (1+panning0)
        ;
        samples[0][i], samples[1][i] = stereoWiden(samples[0][i], samples[1][i], width)
        samples[0][i], samples[1][i] = phaseRotate(samples[0][i], samples[1][i])
        samples[0][i], samples[1][i] = surround(samples[0][i], samples[1][i])
        if (apmix >= 0) then 
            samples[0][i], samples[1][i] = samples[0][i], apmix * filter.process(samples[1][i]) + (1-apmix) * samples[1][i] 
        else
            samples[0][i], samples[1][i] = apmix * filter.process(samples[0][i]) + (1-apmix) * samples[0][i], samples[1][i] 
        end
        samples[0][i], samples[1][i] = 
            samples[0][i] * (1-panning),
            samples[1][i] * (1+panning)
        ;
        if (monofreq >= 20) then
            lSample, rSample = filterMono.process(samples[0][i]), filterMono2.process(samples[1][i])
            samples[0][i], samples[1][i] = lSample-samples[0][i], rSample-samples[1][i]
            lSample, rSample = stereoWiden(lSample, rSample, 0)
            samples[0][i], samples[1][i] = 
                samples[0][i]*0.5+lSample+samples[0][i]*0.5, 
                samples[1][i]*0.5+rSample+samples[1][i]*0.5
        end
        if (swaplr == "on") then
            samples[0][i], samples[1][i] = samples[1][i], samples[0][i]
        end
    end
end
 
params = plugin.manageParams {
	{
		name = "Pre Pan";
		min = -100;
		max = 100;
		default = 0;
		changed = function(val) panning0=val/100 end;
	},
    {
    	name = "Width %";
        min = 0;
        max = 250;
        default = 100;
        changed = function(val) width=val/200 end;
    },
    {
    	name = "(Surround)";
        min = 0; 
        max = 100;
        default = 0;
        changed = function(val) sr=val/100 end;
    },
    {
    	name = "Phase";
        min = -90;
        max = 90;
        default = 0;
        changed = function(val) phase=(-val)/180 end;
    },
	{
		name = "AP freq";
		min = 10;
		max = 20000;
		default = 440;
		changed = function(val) filter.update{f=val} end;
	},
	{
		name = "AP rez";
		min = 0.1;
		max = 5;
		default = 0.1;
		changed = function(val) filter.update{Q=val} end;
	},
	{
		name = "AP mix";
		min = -100;
		max = 100;
		default = 0;
		changed = function(val) apmix=val/100 end;
	},
	{
		name = "Pan";
		min = -100;
		max = 100;
		default = 0;
		changed = function(val) panning=val/100 end;
	},
	{
		name = "Mono freq";
		min = 0;
		max = 20000;
		default = 440;
		changed = function(val) monofreq=val;filterMono.update{f=val};filterMono2.update{f=val} end;
	},
	{
		name = "Swap L/R";
		type = "list";
		values = {"off"; "on"};
		default = "off";
		changed = function(val) swaplr = val end;
	};
}

Paste it into lua protoplug. I guess you also could highpass the side, but this just is magic!

If you carefully setup it up and are using mono checks in between, then I think you can actually get quite good results with this.


(ffx) #14

This part seems to be extremely slow, I am not sure why, maybe using math lib is very slow?

function phaseRotate(in_left, in_right)
    local phasepi = phase * math.pi
    out_left   = (in_left *  math.cos(phasepi))  - (in_right * math.sin(phasepi + math.pi));
    out_right = (in_left * -math.sin(phasepi)) - (in_right * math.cos(phasepi + math.pi));
    return out_left, out_right
end

Only in one Renoise project, not in Bitwig…?¿?


(Zer0 Fly) #15

maybe try to make local copies of sin/cos/pi in initialisation could make it work better?

Also could it maybe be optimised as “sin(phase+pi) = -sin(phase)” and “cos(phase+pi) = -cos(phase)”, saving a sin and a cos operation… Being save from sin is always a good operation.


(ffx) #16

Thanks for those optimizations, but I don’t think this is the problem, must be kind of denormals that happened. Now I even cannot reproduce it anymore… Weird.