RS-MET icon indicating copy to clipboard operation
RS-MET copied to clipboard

Upsampling/Oversampling for PhaseScope/PrettyScope

Open elanhickler opened this issue 7 years ago • 114 comments

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.

elanhickler avatar Jul 29 '17 05:07 elanhickler

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 avatar Jul 29 '17 12:07 RobinSchmidt

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 avatar Jul 29 '17 12:07 RobinSchmidt

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 avatar Jul 29 '17 12:07 RobinSchmidt

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 avatar Jul 29 '17 13:07 RobinSchmidt

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

RobinSchmidt avatar Jul 29 '17 14:07 RobinSchmidt

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);
    	}
    }
}

elanhickler avatar Jul 30 '17 12:07 elanhickler

are you suggesting to use this as upsampling filter?

RobinSchmidt avatar Jul 30 '17 19:07 RobinSchmidt

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

RobinSchmidt avatar Jul 30 '17 20:07 RobinSchmidt

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!

elanhickler avatar Jul 30 '17 20:07 elanhickler

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

RobinSchmidt avatar Jul 31 '17 09:07 RobinSchmidt

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).

elanhickler avatar Sep 01 '17 04:09 elanhickler

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 avatar Sep 02 '17 13:09 RobinSchmidt

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

RobinSchmidt avatar Sep 02 '17 14:09 RobinSchmidt

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

elanhickler avatar Sep 02 '17 15:09 elanhickler

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

elanhickler avatar Sep 02 '17 15:09 elanhickler

image

elanhickler avatar Sep 02 '17 15:09 elanhickler

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

RobinSchmidt avatar Sep 02 '17 15:09 RobinSchmidt

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

elanhickler avatar Sep 02 '17 15:09 elanhickler

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 avatar Sep 02 '17 15:09 RobinSchmidt

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 avatar Sep 02 '17 15:09 RobinSchmidt

...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)

RobinSchmidt avatar Sep 02 '17 16:09 RobinSchmidt

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.

elanhickler avatar Sep 02 '17 16:09 elanhickler

...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 avatar Sep 02 '17 16:09 RobinSchmidt

Bah, just tried fourpolefilter

what kind of four pole filter?

RobinSchmidt avatar Sep 02 '17 16:09 RobinSchmidt

your FourPoleFilter.

Is there a steep slope filter class I could try?

elanhickler avatar Sep 02 '17 16:09 elanhickler

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 avatar Sep 02 '17 16:09 RobinSchmidt

your FourPoleFilter.

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

RobinSchmidt avatar Sep 02 '17 16:09 RobinSchmidt

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

RobinSchmidt avatar Sep 02 '17 16:09 RobinSchmidt

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

elanhickler avatar Sep 02 '17 16:09 elanhickler

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 avatar Sep 02 '17 16:09 RobinSchmidt

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

RobinSchmidt avatar Sep 02 '17 16:09 RobinSchmidt

what is the class called?

elanhickler avatar Sep 02 '17 16:09 elanhickler

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

RobinSchmidt avatar Sep 02 '17 16:09 RobinSchmidt

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

RobinSchmidt avatar Sep 02 '17 16:09 RobinSchmidt

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

elanhickler avatar Sep 02 '17 16:09 elanhickler

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

elanhickler avatar Sep 02 '17 16:09 elanhickler

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 avatar Sep 02 '17 16:09 elanhickler

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 avatar Sep 02 '17 16:09 elanhickler

What is the difference between order and numStages?

elanhickler avatar Sep 02 '17 16:09 elanhickler

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 avatar Sep 02 '17 16:09 RobinSchmidt

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

RobinSchmidt avatar Sep 02 '17 16:09 RobinSchmidt

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

elanhickler avatar Sep 02 '17 16:09 elanhickler

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 avatar Sep 02 '17 16:09 RobinSchmidt

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 avatar Sep 02 '17 16:09 RobinSchmidt

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

RobinSchmidt avatar Sep 02 '17 16:09 RobinSchmidt

...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)

RobinSchmidt avatar Sep 02 '17 16:09 RobinSchmidt

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.

elanhickler avatar Sep 02 '17 16:09 elanhickler

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

RobinSchmidt avatar Sep 02 '17 16:09 RobinSchmidt

uhhhhhhhhh I wrote the code for you already.

elanhickler avatar Sep 02 '17 17:09 elanhickler

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

RobinSchmidt avatar Sep 02 '17 17:09 RobinSchmidt

class SmootherManager
{
public:
    SmootherManager() {}
    juce::Array<smoothedParameter *> CurSmoothingList;
    void add(smoothedParameter * sp)
    {
        CurSmoothingList.addIfNotAlreadyThere(sp);
    }

    void remove(smoothedParameter * sp)
    {
        CurSmoothingList.removeFirstMatchingValue(sp);
    }

    void doSmoothing()
    {
        for (auto & sp : CurSmoothingList)
            sp->incValue();
    }
};

the object adds itself to the manager, so you need to instantiate the manager and give it a pointer to that so it can add/remove itself.

add itself when value changed remove itself when value stops changing, or the difference is like... 0.000001. in that case, set the smoother's internal state to the target value, don't leave it at a difference obviously.

elanhickler avatar Sep 02 '17 17:09 elanhickler

yes, sounds like a good design. moreover, we could dynamically attach different kinds of smoothers if they have a common baseclass (exponential, gaussian, linear, whatever)

RobinSchmidt avatar Sep 02 '17 17:09 RobinSchmidt

class Smoother, subclasses: ExponentialSmoother, GaussianSmoother etc. the SmoothingManager would hold an array of pointers to the baseclass. ...or something

RobinSchmidt avatar Sep 02 '17 17:09 RobinSchmidt

perhaps Smoothers should be applicable to any kind of value (not only Parameters). we could have a class: SmoothableValue and a Smoother would operate on an object of that class. a SmoothableParameter would be subclass of SmoothableValue and Parameter (multiple inheritance)

RobinSchmidt avatar Sep 02 '17 17:09 RobinSchmidt

whyyyyyyyyyy image image image image

elanhickler avatar Sep 02 '17 17:09 elanhickler

every time I run the debugger, i crash at a different point for engineersfilter

elanhickler avatar Sep 02 '17 17:09 elanhickler

ok I finally got it to work, I think it will crash randomly, and bessel filter is really nice for smoothing, the only problem is that changing the smoothing amount causes things to get weird... like delays in movement, values move on its own, values jumping around (even if no smoothing is going on).

edit: probably related to the fact that prettyscope crashes half the time using EngineersFilter

elanhickler avatar Sep 02 '17 17:09 elanhickler

for smoothing mouse movements, exponentialsmoother vs bessel filter is like night and day.

elanhickler avatar Sep 02 '17 18:09 elanhickler

uuhh - random crashes? might this be a threading issue? i think, i will need to check this out in the proper context. changing the smoothing amount? is this the "cutoff-frequency" (i.e. the transition time)?. if so, i'm not surprised, if things go weird. this filter is not really made for "modulating the cutoff frequency", so to speak.

i'd still think, we may want to look into an optimized chain of 1st order lowpasses. this will probably solve the modulation weirdness and make the whole thing more lightweight

RobinSchmidt avatar Sep 02 '17 18:09 RobinSchmidt

yes, run PrettyScope debugger. Half the time it crashes and doesn't load because of some issue in EngineersFilter code.

elanhickler avatar Sep 02 '17 18:09 elanhickler

Can you get something working for my mouse smoothing? Really would like to have a better smoother for PrettyScope mouse sooner than later to impress customers. The problem is Engineer'sFilter doesn't work (just crashes PrettyScope). Bessel filter would work perfectly, but I can't use it due to crashes.

elanhickler avatar Sep 04 '17 04:09 elanhickler

wtf is this?

smoother.setTargetValue(*this);
//return smoother.getSample();
return smoother2.getSampleDirect1(*this);

you are passing a de-referenced this-pointer as function argument? into functions that take a double value as input?!

RobinSchmidt avatar Sep 04 '17 15:09 RobinSchmidt

how/why does that even compile? :-O

RobinSchmidt avatar Sep 04 '17 15:09 RobinSchmidt

*this converts to jura::parameter::getValue();

did you not read my explanation on how my smoothing system works?

here: https://github.com/RobinSchmidt/RS-MET/issues/66#issuecomment-326500209

elanhickler avatar Sep 04 '17 16:09 elanhickler

ahh..ohh. ok, i see. so there's an implicit conversion from the "myparams" object to double due to having defined a double() operator? uuuuhhh.....that's....well.....let's say....non-obvious. i might say confusing. i'm even tempted to say obfuscated. i had initially only a glance at the code because i found it confusing and really trying hard to get my head around it would have been a distraction from what i was doing at the time (the mod-system). i'll take a closer look soon

RobinSchmidt avatar Sep 05 '17 01:09 RobinSchmidt

but anyway, i think, i could write a smoother class for you based on EngineersFilter which has the same interface as the ExponentialSmoother (plus some additional setOrder function, maybe also setShape - for later adding gaussian). i think, setting it to 1st order would reduce to the ExponentialSmoother. a 1-pole bessel-filter is the same as a one-pole butterworth, gaussian...or just simple rc filter. the differences between the shapes are only apparent for higher orders

btw. i never had any crahses. i tried to comment out the Exp-smoother stuff and uncomment the engineers-filter stuff. can you give me a version of the code which exposes the crash? i need to see it myself to figure out what's going on

RobinSchmidt avatar Sep 05 '17 01:09 RobinSchmidt

I just committed a build "this build breaks prettyscope on purpose" with the filter enabled.

but you might run into this build issue: https://github.com/RobinSchmidt/RS-MET/issues/71

elanhickler avatar Sep 05 '17 03:09 elanhickler

ok - i'll check that tomorrow

RobinSchmidt avatar Sep 05 '17 03:09 RobinSchmidt

ok, don't use the latest commit, use the commit:

"fixed build issues! yay (Prettyscope still broken on purpose, do not use this commit if your name is not Robin!)" daecfea937ecac27fbdd42718f517a184135f4b6

You know how to choose a commit, right?

elanhickler avatar Sep 05 '17 04:09 elanhickler

yes, i think so. it's not something i use a lot, though.

RobinSchmidt avatar Sep 05 '17 04:09 RobinSchmidt

ok - i think, i know what the culprit is. you do things like:

	paramStrIds =
	{
		&(FXMode = myparams(i++, BUTTON, "FXMode", 0.0, 1.0, 0.0)),
		&(Pause = myparams(i++, BUTTON, "Pause", 0.0, 1.0, 0.0)),

where you use the assignment operator = on objects of class type myparams. that implies that such objects are getting copied. you are actually creating temporary objects here myparams(i++, BUTTON, "FXMode", 0.0, 1.0, 0.0) and then assign them to the lhs FXMode = . after the assignment, the temp objects are deleted. when they contain other objects like my EngineersFilter, those get copied too - using the implicitly compiler-generated assignment operator. but the EngineersFilter class contains pointer variables, so the auto-generated trivial assignment operator is not suitable. it creates a shallow copy but we would need a deep copy:

https://en.wikipedia.org/wiki/Object_copying#Shallow_copy

RobinSchmidt avatar Sep 05 '17 10:09 RobinSchmidt

i could manually implement a more suitable assignment operator in EngineersFilter - but actually, i do not intend my dsp objects to be copyable/assignable like variables. things like

EngineersFilter filter2 = filter1;

do not seem to make much sense to me. you could instead of having an EngineersFilter member a pointer-to-EngineersFilter and create the actual object with new in the constructor of myparams and delete it in the destructor (or use a smart-pointer). this way, not the filter itself would get copied but only the pointer-to-it, which should work. i could perhaps explicitly disallow copying of such filter objects such that compiler would complain when you try to do this

RobinSchmidt avatar Sep 05 '17 10:09 RobinSchmidt

wait...no... i think, using a pointer still wouldn't work

RobinSchmidt avatar Sep 05 '17 10:09 RobinSchmidt

I got something working, but can't really use engineers filter due to values jumping around when setting smoothing amount. Sometimes the value jumps around and then is permanently offset. Seems like the Jura plugin doesn't have this problem, strange.

So don't worry about this, will have to wait until you make a new filter class.

elanhickler avatar Sep 05 '17 14:09 elanhickler

I got something working

i think, the cleanest solution woud be to get rid of using the = operator here:

FXMode = myparams(i++, BUTTON, "FXMode", 0.0, 1.0, 0.0)

by replacing these assignments with something like:

FXMode.init(i++, BUTTON, "FXMode", 0.0, 1.0, 0.0)

this way, you would also avoid the wasteful creation and copying of temporary objects

RobinSchmidt avatar Sep 05 '17 16:09 RobinSchmidt

ok ill make the change.

so this new gaussian filter you want to make... it could become my favorite filter, bessel filter has no ring. all other filters have ringing. Essentially it's the cleanest possible filter. I'd want to experiment with this filter for general audio/musical filtering purposes, assuming this new filter will be modulatable.

elanhickler avatar Sep 05 '17 17:09 elanhickler

i have now declared the copy-constructor and assignment operator as "deleted" in rsEngineersFilter, so now the compiler will balk on any attempt to copy rsEngineersFilter objects. i should really use this idiom consistently throughout the whole library for any class that can't be trivially copied...

i could make a dedicated smoothing filter based on a chain of 1st order lowpasses. eventually, this would also approach a gaussian shape with increasing order (by virtue of the central limit theorem), but however, it would presumably approach it not as quickly as a real gaussian filter design. so a real gaussian design would be a new design formula to be added to EngineersFilter (i have the formula in a book here). but that wouldn't be modulatable. i guess, i should prefer the 1st order lowpass chain then (i think, it would be modulatable)

RobinSchmidt avatar Sep 05 '17 17:09 RobinSchmidt

bessel filter has no ring. all other filters have ringing. Essentially it's the cleanest possible filter.

the design goal in the bessel filter is to make the phase response as close to linear as possible which means making the impulse response as symmetrical as possible. the plots in my book suggest that is has a tiny amount of overshoot. i think, a true gaussian would have none but be otherwise very similar

RobinSchmidt avatar Sep 05 '17 17:09 RobinSchmidt

There's a ton of discussion on the smoothing filter here.

Get started on the upsampling thing for PrettyScope when you can.

elanhickler avatar Aug 31 '18 22:08 elanhickler

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.

yes - that's what i have in mind. my plan is to record 4 successive samples and put a cubic spline between the two inner ones such that at the joints (sample-points), the derivatives are matched (i.e. use a "hermite interpolation spline"). i describe that here:

http://www.rs-met.com/documents/dsp/TwoPointHermiteInterpolation.pdf

we would only use the 1st derivative and hence a cubic polynomial. the values of the derivatives (which are inputs to the hermite spline formula) are computed from differences of adjacent samples (we would use a finite difference numerical approximation to the derivative to supply target derivative-values to the formula). it would introduce a delay/latency of one sample because to find the desired value for the derivative at the "now" sample, i would need one future sample (no issue at all in this case). but: i can straightforwardly implement that spline drawing in the prototype cpu scope - but not in OpenGL. there, the most low-level rendering primitive is already the line (i would set single pixels inside my spline drawing function). but maybe i should do it like that anyway and we may later approximate my spline drawing prototype with (short) lines in OpenGL. edit: well, actually i think, we could also draw points in OpenGL to simulate my single pixel drawing (those "paintDot" functions that i would call in my (to be written) spline drawing routine). but that might be too inefficient. but maybe not.

...if you wonder why i dig up such an old quote, i've just been re-reading the whole thread in full

RobinSchmidt avatar Sep 01 '18 02:09 RobinSchmidt

If you can't make it work for PrettyScope it is not useful to me. I thought we would ultimately just add more data points to the audio buffer. Also, that's why I posted the full code of dood.al oscilloscope so you could check how they are doing it. And if they are in fact doing something fancy in OpenGL then I don't expect you to do this for me. It looks like they are adding more data points.

elanhickler avatar Sep 01 '18 03:09 elanhickler

it's not too inefficient to draw only dots btw. I want to actually move toward doing that for a high quality mode for PrettyScope.

elanhickler avatar Sep 01 '18 04:09 elanhickler

that's why I posted the full code of dood.al oscilloscope so you could check how they are doing it. [..] It looks like they are adding more data points.

you mean this: generateSmoothedSamples : function (oldSamples, samples, smoothedSamples) function? add more datapoints to the audio buffer in the sense of giving an oversampled signal? obtaining an oversampled signal is something that my spline-interpolation approach could also do. you would just have to evaluate the spline at intermediate positions. i.e. when the algorithm receives a new sample, it could produce 4 new output samples (if you want to have 4x oversampling, for example). but it's something that a bessel-filter could also do already, so you wouldn't necessarily need new code for that. the spline oversampler may have some advantages though - such as passing through the original samples. just adjusting a bessel filter to sr/4 and feeding it one sample (and 3 zeros) for every input sample would not guarantee that the oversampled signal values at the sample-points (of the original signal) are identical to the values at the same time-instants in the oversampled signal. ...sooo - maybe what you need is a spline-based upsampler class? something like my EllipticSubBandFilter...but as SplineSubBandfilter

i have to admit that i didn't really reverse engineer the code you posted. but the mentioning of a lanczos filter is something that i recognize from the world of image processing. it is actually an interpolation method:

https://en.wikipedia.org/wiki/Lanczos_resampling

RobinSchmidt avatar Sep 01 '18 04:09 RobinSchmidt

it's not too inefficient to draw only dots btw

that's good. then maybe my original idea of the spline drawing can be used. my algorithm would then tell you where to draw the dots. you would feed it an input sample pair and it would spit out a number of dot-coordinates to draw (the number of dots may vary from sample to sample, depending on how far apart successive sample-points are)

RobinSchmidt avatar Sep 01 '18 04:09 RobinSchmidt

i have already put a stub for the relevant function into the codebase. here's the roadmap:

template<class TPix, class TWgt, class TCor>
void rsImagePainter<TPix, TWgt, TCor>::drawDottedSpline(TCor x1, TCor x1s, TCor y1, TCor y1s, 
  TCor x2, TCor x2s, TCor y2, TCor y2s, TPix color, TCor density, int maxNumDots, 
  bool scaleByNumDots)
{
  // Not yet implemented. Here is what we would have to do:
  // -compute coeffs of the two polynomials:
  //  x(t) = a0 + a1*t + a2*t^2 + a3*t^3
  //  y(t) = b0 + b1*t + b2*t^2 + b3*t^3
  // -compute the total length of the spline segment (this will be some kind of analytic line 
  //  integral) to be used to scale the brightness of the dots
  // -compute a sequence of t-values at which to evaluate the polynomials and set a dot - these
  //  t-values should be chosen such that the spline segments between successive t-values have
  //  all the same length, t should be in the range 0..1
  // -the rest is conceptually similar to drawDottedLine
}

sooo..that's now what to do...

RobinSchmidt avatar Sep 04 '18 16:09 RobinSchmidt

ok...making some progress. the connections are smooth now ...but wiggly. i think my data points and derivatives are out of sync. will fix that tomorrow...

RobinSchmidt avatar Sep 09 '18 21:09 RobinSchmidt

yessss! old - connecting the dots with lines:

image

new - connecting the dots with smoooth splines:

image

ignore the artifact at the center - must have to do with initial conditions and should be irrelevant in realtime mode (i rendered these pics non-realtime using the rsPhaseScopeBuffer class (the data-points are exactly the same in both renderings) - still need to incorporate the new mode into the Scope - not sure, if i should make it switchable or always just use spline rendering)

RobinSchmidt avatar Sep 10 '18 18:09 RobinSchmidt

If cpu usage is negligible make it non-optional. Also, if it is negligible, can you make the upsampling algorithm do even more upsampling for even nicer looking results? Or is that not how it works? (like how oversampling can be switched between x2 x4 x8).

Another question, would you be able to use this upsampling inside a synth audio path and get less aliasing? There would be no Gibbs (ripples) like in the antialiasing filter, which is neither good or bad, I would still think to combine it with an antialiasing filter... But the upsampling would perhaps create a smooth (no Gibbs) signal similar to polyblep oscillators but on an arbitrary signal.

Edit: excited to see this in action, can't wait to try it in the rsPhaseScope

elanhickler avatar Sep 10 '18 22:09 elanhickler

yeah..i did not yet measure cpu usage. and i'm not finished yet because i discovered another problem not apparent above. the plots above use 80 datapoints for one single roundtrip through the lissajous figure. reducing it to 35 datapoints gives this: image image ...the shape is still nicely smooth, but i get color discontinuities at the joints. i could trace this to having non-equidistant dots along the splines and having high-density segments join to low-density segments. i'm currently pondering how to remedy this (which may tax the cpu some more, depending on what i come up with....).

can you make the upsampling algorithm do even more upsampling for even nicer looking results? Or is that not how it works?

no, that's not how it works. i'm just connecting incoming datapoints with spline segments which are conceptually continuous between the datapoints (just as the lines are).

would you be able to use this upsampling inside a synth audio path and get less aliasing? There would be no Gibbs (ripples) like in the antialiasing filter

this should indeed be possible. actually, it would just be cubic-spline interpolation upsampling

RobinSchmidt avatar Sep 10 '18 22:09 RobinSchmidt

the math has become a bit nasty. in order to get the total length of the spline arc, i have to evaluate this integral (in the cyan box):

http://tutorial.math.lamar.edu/Classes/CalcII/ParaArcLength.aspx

from t=0 to t=1 (t is my spline parameter), where the term under the square root boils down to a quartic polynomial. the analytic formula for this integral is too complicated for wolfram:

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

i could find analytic formulas for quadratic polynomials under the square root - but even these formulas were a mess. and for a cubic, all computer algebra systems i tried throw the towel. ...so i have to resort to numerical integration. maybe it's possible to find an analytic formula - but that would be too messy to be useful anyway. moreover, i don't only need the total arc length but also the inverse function of the function which is defined by this integral (by varying the upper integration limit "beta" keeping alpha=0)......but i'm almost there. i've implemented a nice numerical integration function...now i need to invert the resulting function...almost there...this is fun! i love these kinds of problems.

RobinSchmidt avatar Sep 13 '18 01:09 RobinSchmidt

oh god kill me.

edit: just let me know, what implications does this have for implementing inside prettyscope?

elanhickler avatar Sep 13 '18 03:09 elanhickler

it just makes it a bit complicated for me to compute the positions at which we should draw/accumulate dots. with a line, we would just take the total distance between two points (i'll now call the incoming data "points" and the to-be-drawn dots "dots"), get coeffs for a line equation between the points and draw some number of dots along this line. now, we do the same thing but with a spline (obtain spline-coeffs and put dots along this spline) this is what already works and what the pics show. the thing is that with a line, the dots along the line are naturally equidistant - but with a spline, they are not. naively incrementing the parameter t by a fixed stepsize leads to non-equidistant dots, like this (this is a single test spline segment): image which may lead to these color mismatches at the joints of segments. i'm trying to figure out how to increment my parameter t in unequal amounts so as to have equidistant dots. that's the inversion of the integral....you don't have to worry about this - my function will just spit out the dot-positions for you

RobinSchmidt avatar Sep 13 '18 04:09 RobinSchmidt

looks like i have the math working. the dots are now equidistant:

image

that was a fun challenge and resulted in some new math code for the library which may be useful for other things, too. now, i have a lot of cleaning up, refactoring and optimizing to do

RobinSchmidt avatar Sep 13 '18 13:09 RobinSchmidt

uhhhhhhhh would it be easy to enable in the toolchain phase scope so I can see how it looks before you do the clean up?

elanhickler avatar Sep 13 '18 19:09 elanhickler

ok - next to the AntiAlias button there's now a new menu with two entries for the interpolation mode: "Linear" (as before) and "Cubic". currently, the cubic mode does not yet use the density compensation (i.e. the nasty math). i'll probably make that optional (because i guess, it's going to be much more expensive than the spline-drawing itself)

will refactor now. i think, the interpolation functionality which is now scattered over the two classes rsImagePainter and rsPhasescopeBuffer should be consolidated in its own dedicated class. this will also facilitate a later port to OpenGL. you will then have to use dot-drawing in OpenGL instead of line-drawing (although, i may also add a hybrid mode that interpolates with splines and uses (short) lines to connect points on the spline - we'll see...)

RobinSchmidt avatar Sep 14 '18 06:09 RobinSchmidt

cannot open file graphics/realtimespline

rebuilding the toolchain visual studio with jucer didn't work.

how did it manage to not be in the folder?

image

elanhickler avatar Sep 14 '18 08:09 elanhickler

oh because you just added an include and you didn't actually add the file, you must be intending to but didn't do it yet. Commenting out the include got it working.

elanhickler avatar Sep 14 '18 08:09 elanhickler

dood.al proof (interpolated)

image

your result (interpolated)

some differences can be seen like the O shape at the upper right. The proof seems to be more chaotic looking, not as clean as yours. image

proof (perfect)

image

ugly!!!!

image

elanhickler avatar Sep 14 '18 08:09 elanhickler

yours / dood.al / proof

image image image

Seems like your upsampling is more accurate, less wiggles.

elanhickler avatar Sep 14 '18 09:09 elanhickler

you didn't actually add the file, you must be intending to but didn't do it yet.

oh - right. fixed that. i just added it to the repo. that's where i want to factor out all the code that is related to the 2D spline interpolation which is now scattered over two classes

what do you mean by "proof" and "perfect"? and what is the right image in the last post? linear interpolation? or is "proof" juts the same signal scoped at a higher sample-rate/lower frequency?

Seems like your upsampling is more accurate, less wiggles.

i guess, the wiggles come from dood's lanczos kernel dropping below 0: https://en.wikipedia.org/wiki/File:Lanczos-kernel.svg ...but i actually have no idea, what my "kernel" looks like, because i think about the problem in different terms, but i may try to create some plots..

RobinSchmidt avatar Sep 14 '18 18:09 RobinSchmidt