RobinSchmidt / RS-MET

Codebase for RS-MET products (Robin Schmidt's Music Engineering Tools)
Other
57 stars 6 forks source link

Upsampling/Oversampling for PhaseScope/PrettyScope #57

Open elanhickler opened 7 years ago

elanhickler commented 7 years ago

No idea what this feature involves, but you said you could add a feature to remove unrealistic jagged lines, sharp corners, etc. As seen here: https://www.youtube.com/watch?v=SvCfz-TY6Go

I will gladly pay for your time on this, I'd like to have it as soon as possible, assuming it's not a big investment.

RobinSchmidt commented 7 years ago

i was thinking about connecting the incoming signal "dots" not with straight lines but cubic spline segments. it's a bit of math to work out, i'm not sure, how hard this will be, maybe a few hours (i also want to compute the exact lengths of the spline segments for brightness division - i guess, i'll have to do a line integral for this). however, i'm not sure how you would draw splines in OpenGL - i can do it straightforwardly in PhaseScope because i do all the line-drawing myself there anyway (i would just replace the dotted-line-drawing algorithm with a dotted-spline-drawing algorithm). but in OpenGL, the lowest level you can access is the line-drawer (i think)....unless you also draw lines with dots which will be expensive. so, i'm not sure, how easily it will translate. ....maybe for PrettyScope, straightforward oversampling with lowpass filtering might be the better approach

RobinSchmidt commented 7 years ago

i guess, i'll have to do a line integral for this

something like that:

http://tutorial.math.lamar.edu/Classes/CalcIII/LineIntegralsPtI.aspx

but that's really the 2nd step. first i need to have parametric formulas for the spline segment itself. i guess, i can just use the regular cubic spline interpolation formulas for x and y separately. ...hopefully

RobinSchmidt commented 7 years ago

using an oversampling approach would just mean to compute more intermediate data points and then connecting them with line segments. this would also smooth the curve, but is arguably less elegant. ...but might be the only way to do it in OpenGL anyway

RobinSchmidt commented 7 years ago

ah - no - this is even more relevant: http://tutorial.math.lamar.edu/Classes/CalcII/ParaArcLength.aspx the link above is for more general stuff, this is a simpler special case which applies here

RobinSchmidt commented 7 years ago

i think, it (the formula in cyan) will eventually boil down to such an integral:

http://www.wolframalpha.com/input/?i=integral+sqrt(a_0+%2B+a_1+t+%2B+a_2+t%5E2+%2B+a_3+t%5E3+%2B+a_4+t%5E4)+dt+from+0+to+1

because the derivative of a cubic is a quadratic, squaring it yields a quartic - so under the square root we'll get a sum of two quartics which is still a quartic. unfortunately, it exceeds standard computation time of wolfram alpha. ...but i'm really trying to do the second step before the first here. so, nevermind

elanhickler commented 7 years ago

I stole this from dood.al if it helps. Also apparently the license for this is "free to use, sell, etc"

I posted full source code of dood.al oscilloscope in my Chaosfly git.

var Filter =
{
    lanczosTweak : 1.5,

    init : function(bufferSize, a, steps)
    {
        this.bufferSize = bufferSize;
        this.a = a;
        this.steps = steps;
        this.radius = a * steps;
        this.nSmoothedSamples = this.bufferSize*this.steps + 1;
        this.allSamples = new Float32Array(2*this.bufferSize);

        this.createLanczosKernel();
    },

    generateSmoothedSamples : function (oldSamples, samples, smoothedSamples)
    {
        //this.createLanczosKernel();
        var bufferSize = this.bufferSize;
        var allSamples = this.allSamples;
        var nSmoothedSamples = this.nSmoothedSamples;
        var a = this.a;
        var steps = this.steps;
        var K = this.K;

        for (var i=0; i<bufferSize; i++)
        {
            allSamples[i] = oldSamples[i];
            allSamples[bufferSize+i] = samples[i];
        }

        /*for (var s= -a+1; s<a; s++)
        {
            for (var r=0; r<steps; r++)
            {
                if (r==0 && !(s==0)) continue;
                var kernelPosition = -r+s*steps;
                if (kernelPosition<0) k = K[-kernelPosition];
                else k = K[kernelPosition];

                var i = r;
                var pStart = bufferSize - 2*a + s;
                var pEnd = pStart + bufferSize;
                for (var p=pStart; p<pEnd; p++)
                {
                    smoothedSamples[i] += k * allSamples[p];
                    i += steps;
                }
            }
        }*/

        var pStart = bufferSize - 2*a;
        var pEnd = pStart + bufferSize;
        var i = 0;
        for (var position=pStart; position<pEnd; position++)
        {
            smoothedSamples[i] = allSamples[position];
            i += 1;
            for (var r=1; r<steps; r++)
            {
                var smoothedSample = 0;
                for (var s= -a+1; s<a; s++)
                {
                    var sample = allSamples[position+s];
                    var kernelPosition = -r+s*steps;
                    if (kernelPosition<0) smoothedSample += sample * K[-kernelPosition];
                    else smoothedSample += sample * K[kernelPosition];
                }
                smoothedSamples[i] = smoothedSample;
                i += 1;
            }
        }

        smoothedSamples[nSmoothedSamples-1] = allSamples[2*bufferSize-2*a];
    },

    createLanczosKernel : function ()
    {
        this.K = new Float32Array(this.radius);
        this.K[0] = 1;
        for (var i =1; i<this.radius; i++)
        {
            var piX = (Math.PI * i) / this.steps;
            var sinc = Math.sin(piX)/piX;
            var window = this.a * Math.sin(piX/this.a) / piX;
            this.K[i] = sinc*Math.pow(window, this.lanczosTweak);
        }
    }
}
RobinSchmidt commented 7 years ago

are you suggesting to use this as upsampling filter?

RobinSchmidt commented 7 years ago

if we do oversampling, i'd probably rather just throw in a bessel filter

elanhickler commented 7 years ago

No, I am simply letting you know what the code is behind dood.al just in case you wanted a working example. I wish PrettyScope looked exactly like it (minus a few things, plus I need lots more features), but use whatever algorithm you want, as long as it works!

RobinSchmidt commented 7 years ago

ok, we'll see. i'm offline now until sunday

elanhickler commented 7 years ago

Just want to ask about something. I added mouse control to the canvas of PrettyScope essentially turning it into an X/Y pad that can also output your mouse movements as audio (using audio fx mode). However, it doesn't looks as nice as I'd like, because the mouse is only updated so often and you get little corners despite smoothing.

If/when you implement this oversampling thing for PrettyScope, will it be relevant to smooth 2d mouse movements? Maybe some extreme version of what you'd make, or extreme smoothing settings (since mouse movements and audio rate movements are very different speeds).

RobinSchmidt commented 7 years ago

not only do they have a different speed/rate but also are mouse movements potentially (and likely) non-equidistant which pretty much rules out any common filtering technique for upsampling the mouse events. hmmm...actually, when you apply smoothing to the shiftX/Y parameters (as they result from mouse-movements), what you do is to sample-and-hold the incoming values (whenever they come in, irregularly) and then apply a 1st order lowpass to that S-H-signal. you could perhaps replace that 1st order lowpass by a 2nd order one to get rid of corners.

RobinSchmidt commented 7 years ago

well, if your use rosic::ExponentialSmoother for smoothing, this class actually encapsulates the sample-and-hold process and the 1st order lowpass into one convenient object. you could simply put another lowpass after it, for example, a rosic::OnePoleFilter.

i could even further encapsulate that into a class myself, like rosic::SecondOrderSmoother or something

elanhickler commented 7 years ago

What's the difference between onepole and exponentialsmoother?... oh, it has a sample & hold.

elanhickler commented 7 years ago

I need a "get set target value" / "get current value" function. OnePole filter only has getSample.

elanhickler commented 7 years ago

image

RobinSchmidt commented 7 years ago

smoother2 doesn't need a target value. it always gets an input that has the S/H already applied. think of it as a 3 step process:

1: sample-and-hold (i.e. update the target-value and use it as constant output) 2: lowpass 1 3: lowpass 2

step 1 and 2 are alamgamated into ExponentialSmoother. lowpass 2 simply takes the output of the 1st lowpass. it doesn't have its own target value as it doesn't do any additional sample-and-hold process.

maybe i should simply implement it myself in rosic, maybe further optimization could be applied by doing it in an almagamated process

elanhickler commented 7 years ago

noooooooooooo, I think I want to try higher order, 2, 4, 8

RobinSchmidt commented 7 years ago

hmm...maybe a general higher order Smoother class could be written that has a setOrder() function. no need to restrict yourself to powers of two, btw.

RobinSchmidt commented 7 years ago

conceptually, it's just sample-and-hold and then apply any number of 1st order lowpasses one after another. of course, some optimizations are possible: if all lowpasses have the same settings, you don't need to have them all store their own cutoff, sample-rate etc. or even coeffs. ...and perhaps one would want to auto-scale the actual time-constant with the order in some way, so the response doesn't get more and more sluggish when ramping up the order

RobinSchmidt commented 7 years ago

...also, it's not necessary that all lowpasses have the same cutoff. it's not even necessarry to use a bunch of 1st order lowpasses - instead a single high-order lowpass could be used. perhaps one that approximates gaussian impulse response. together with sample-and-hold, the transition would approximate a nice sigmoid (the integral of a gaussian bell curve)

elanhickler commented 7 years ago

Bah, just tried fourpolefilter. It's all not good. What I envision is more of an upsampling filter or a realtime bezier system / filter... like... your onepolefilter has an internal sample & hold, I guess instead of using a onepole you would use a system of a history of data points that you interpolate between.

RobinSchmidt commented 7 years ago

...but that might be too fancy. i guess, an auto-scaled series of equal lowpasses should be good enough and more straightforward and efficient

RobinSchmidt commented 7 years ago

Bah, just tried fourpolefilter

what kind of four pole filter?

elanhickler commented 7 years ago

your FourPoleFilter.

Is there a steep slope filter class I could try?

RobinSchmidt commented 7 years ago

instead of using a onepole you would use a system of a history of data points that you interpolate between.

yeah, the problem is that such a thing is difficult to do in realtime, because you might want to use future datapoints now that you don't have yet. for example, to connect two points with a cubic spline, i need to define the positions of the points and the derivatives there. but to assign meaningful values to the derivatives of the current datapoint, i may want to to know the next one (to use the difference of next-current for the derivative)

RobinSchmidt commented 7 years ago

your FourPoleFilter.

that's a bad one for time-domain work. it's two biquads and will produce overshoot ...right?

RobinSchmidt commented 7 years ago

for time domain work, the best would probably be a gaussian. but a series of equal 1st order filters may be not that bad either

elanhickler commented 7 years ago

overshoot? i dunno, I'm just still getting corners. Oh, wouldn't a high order slope cause ringing/overshooting anyway?

RobinSchmidt commented 7 years ago

wouldn't a high order slope cause ringing/overshooting anyway?

that depends on the response type of the filter. butterworth: certainly. elliptic: even more. bessel: much less so. gaussian: probably nicest transition shape. series of 1st order filters: i assume, not that bad either

RobinSchmidt commented 7 years ago

Is there a steep slope filter class I could try?

you could try EngineersFilter with the Bessel response. ..i'd like to add gaussian someday but it's not there yet

elanhickler commented 7 years ago

what is the class called?

RobinSchmidt commented 7 years ago

however, for everyday parameter smoothing work, EngineersFilter may be a bit too expensive

RobinSchmidt commented 7 years ago

rsEngineersFilter - in the filters folder ...yay - i already managed to add the rs prefix there

elanhickler commented 7 years ago

you make it super difficult to find where the MODE enum is.................... what is it for lowpass?

elanhickler commented 7 years ago

oh wait this time you didn't make it difficult, you actually say InfiniteImpulseResponseDesigner::modes in the comments.

elanhickler commented 7 years ago

I wonder if a decorator design would work for engineer's filter with all these modes and types.

myFilter = Bessel(Lowpass(Filter)); myFilter.setCutoff(v); myFilter.getSample(v);

elanhickler commented 7 years ago

except... you'll have a namespace problem.

rosic::FilterType::Bessel(rosic::FilterShape::Lowpass(rosic::FilterBase)); //kinda ugly

Edit: Oh and then you wouldn't be able to easily change the filter shape.

elanhickler commented 7 years ago

What is the difference between order and numStages?

RobinSchmidt commented 7 years ago

i consider the decorator pattern as a convenient way to mix-and-match additional functionalities to a core class and let client code decide which combination of additional features is needed. in subclassing, you could add one feature per subclass like: MyClass < MyClassWithX < MyClassWithXAndY. but you could never get MyClassWithY (without X) unless you make a separate MyClassWithY class. ...so the number of classes you would have to write would explode with the number of independent features, you'd like to add

RobinSchmidt commented 7 years ago

What is the difference between order and numStages?

numStages is the number of biquad stages. each biquad stage has order 2...or maybe 1 if only 1 pole of the biquad is used

elanhickler commented 7 years ago

what do I set? order? and leave numstages alone? im confused!

RobinSchmidt commented 7 years ago

yes, order. ignore the BiquadCascade baseclass for setup. use the EngineersFilter methods only. but you need getSampleDirect1 (or 2) from the baseclass for producing samples

RobinSchmidt commented 7 years ago

the numStages will the selected automatically, according to your desired order (and whether it's a lowpass, or bandpass or whatever).

oh, well you should use setPrototypeOrder

RobinSchmidt commented 7 years ago

...maybe i should not publically inherit from BiquadCascade. most of its methods should not be used in an EngineersFilter object

RobinSchmidt commented 7 years ago

...but as said...this should be only used for some preliminary experimentation. it will be very wasteful to use full-scale EngineersFilter objects for a task as benign as parameter smoothing (of which you supposedly need a lot at the same time - i.e. a large number of smoother objects)

elanhickler commented 7 years ago

you don't need a lot at the same time if you have a smoother manager, you only smooth the controls that the user is changing... that's like...2 at most unless you have really long smoothing times.

RobinSchmidt commented 7 years ago

ahhh...i see...you would not let the parameter-object itself be responsible for smoothing, but kind of attach a smoother object to it, whenever needed. you could keep a smoother-pool around in the smoother manager (https://sourcemaking.com/design_patterns/object_pool) and then re-use the smoothers from there for any parameter that currently needs smoothing. and in the event you run out of smoothers, the mamanger could dynamically create new ones....interesting. i need to think about that design idea

elanhickler commented 7 years ago

uhhhhhhhhh I wrote the code for you already.

RobinSchmidt commented 7 years ago

btw. i just discovered this nice pattern site a few days ago. it has a nice collection of design pattern descriptions