RS-MET
RS-MET copied to clipboard
Upsampling/Oversampling for PhaseScope/PrettyScope
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.
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
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
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
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
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
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);
}
}
}
are you suggesting to use this as upsampling filter?
if we do oversampling, i'd probably rather just throw in a bessel filter
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!
ok, we'll see. i'm offline now until sunday
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).
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.
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
What's the difference between onepole and exponentialsmoother?... oh, it has a sample & hold.
I need a "get set target value" / "get current value" function. OnePole filter only has getSample.
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
noooooooooooo, I think I want to try higher order, 2, 4, 8
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.
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
...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)
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.
...but that might be too fancy. i guess, an auto-scaled series of equal lowpasses should be good enough and more straightforward and efficient
Bah, just tried fourpolefilter
what kind of four pole filter?
your FourPoleFilter.
Is there a steep slope filter class I could try?
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)
your FourPoleFilter.
that's a bad one for time-domain work. it's two biquads and will produce overshoot ...right?
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
overshoot? i dunno, I'm just still getting corners. Oh, wouldn't a high order slope cause ringing/overshooting anyway?
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
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
what is the class called?
however, for everyday parameter smoothing work, EngineersFilter may be a bit too expensive
rsEngineersFilter - in the filters folder ...yay - i already managed to add the rs prefix there
you make it super difficult to find where the MODE enum is.................... what is it for lowpass?
oh wait this time you didn't make it difficult, you actually say InfiniteImpulseResponseDesigner::modes in the comments.
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);
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.
What is the difference between order and numStages?
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
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
what do I set? order? and leave numstages alone? im confused!
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
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
...maybe i should not publically inherit from BiquadCascade. most of its methods should not be used in an EngineersFilter object
...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)
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.
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
uhhhhhhhhh I wrote the code for you already.
btw. i just discovered this nice pattern site a few days ago. it has a nice collection of design pattern descriptions
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.
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)
class Smoother, subclasses: ExponentialSmoother, GaussianSmoother etc. the SmoothingManager would hold an array of pointers to the baseclass. ...or something
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)
whyyyyyyyyyy
every time I run the debugger, i crash at a different point for engineersfilter
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
for smoothing mouse movements, exponentialsmoother vs bessel filter is like night and day.
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
yes, run PrettyScope debugger. Half the time it crashes and doesn't load because of some issue in EngineersFilter code.
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.
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?!
how/why does that even compile? :-O
*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
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
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
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
ok - i'll check that tomorrow
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?
yes, i think so. it's not something i use a lot, though.
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
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
wait...no... i think, using a pointer still wouldn't work
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.
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
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.
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)
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
There's a ton of discussion on the smoothing filter here.
Get started on the upsampling thing for PrettyScope when you can.
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
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.
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.
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
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)
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...
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...
yessss! old - connecting the dots with lines:
new - connecting the dots with smoooth splines:
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)
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
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:
...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
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.
oh god kill me.
edit: just let me know, what implications does this have for implementing inside prettyscope?
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):
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
looks like i have the math working. the dots are now equidistant:
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
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?
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...)
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?
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.
dood.al proof (interpolated)
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.
proof (perfect)
ugly!!!!
yours / dood.al / proof
Seems like your upsampling is more accurate, less wiggles.
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..