However I'm an ignorant geezer and in addition it has been more than a decade since I last made an equalizer effect. The following may be hopelessly full of errors, read at your own risk. Just some silly novelty ideas or "something you might play with" to see how bad it performs.
Included is some jsfx example code snippets. I have tested the JOS allpass delay parts, but at the moment I don't have a good "EQ suitable" filter object in jsfx so in the following code example in commented-out lines are example use of a fictional peaking filter object to make a crude 2 band EQ just to give the basic idea. If you drop in a working peaking filter object then maybe it would actually work. Or maybe there are logic flaws.
Long ago I used the JOS allpass interpolator as part of an oversampling toolkit but hadn't used it for years. In retrospect I had decided the allpass interpolation was maybe a "fatally flawed" cheap trick.
Then writing a hobby peaklimiter, I tested 4X JOS AP "intersample peak detection" compared to 4X sinc FIR interpolation oversampling. In context of my little peaklimiter, surprisingly the allpass interpolator picked out intersample peak levels very close to the same as sinc oversampling. As measured on music mix test audio, only a fraction of a dB difference, I decided to use the allpass interpolation rather than the FIR oversampling because they measured ISPs "about the same" so far as I could determine on practical mixes, and the allpass interpolation was so much simpler than FIR oversampling.
So I got interested in it again and MAYBE it wouldn't necessarily be so bad a way to cheap oversample for an equalizer or maybe some other thangs.
https://ccrma.stanford.edu/~jos/pasp/Fi ... ation.html I have trouble understanding a lot of JOS articles. In my ignorance the difference equation he writes seems to describe an allpass topology slightly different from the schematic block diagram in the article. Most likely both versions are completely equivalent but I'm too ignorant to judge. So having to choose and not knowing any better, in the code below I copied the flowchart in the article's picture rather than the text difference equation in the article. It seems to work as advertised in my testing.
It borrows some code from the peaklimiter. Only oversamples 4X for lower samplerates, etc. At higher samplerates you don't really need to oversample so much.
https://www.dsprelated.com/freebooks/pa ... ation.html At the top of this article is described linear interpolation which tends to FIR lowpass filter similar to the "repeat-sample" naive oversampling of an IIR filter just by feeding it the same sample 2 or more times in a row.
As shown in both articles, above about 10 kHz at 44.1 k samplerate, the allpass interpolator begins to degrade toward "repeat sample" behavior but not so bad except in the top audio octave. I'm guessing maybe this could maybe add a lowpass characteristic way up high if we use the allpass interpolator for oversampling, just like linear interpolation or repeat-sample oversampling, but I don't think the lowpass would be as extreme. Also, maybe some tracks can contain a lot of energy above 10 kHz, but I think the majority of practical music tracks do not have very much percent of RMS power above 10 kHz.
I haven't tested it, but for instance if this kind of allpass oversampling, its worst effect was a slight high-frequency loss, then maybe it could be compensated with a little bit of high-frequency pre-emphasis or post-emphasis adjustment.
OK here is another thing maybe I'm thinking about it wrong in the pseudo-code below-- The fictional EQ prototype is the kind that takes a center freq in Hz and a samplerate. The actual filter-setting math figures it as the center frequency as a ratio against the sample rate, so initting the filter with a higher samplerate has the effect of tuning the filter lower as a percentage of the samplerate.
When you "temp oversample" a signal it is like transposing the signal down. If the input signal is 1000 Hz and you double the number of samples, then the oversampled "slowed down" frequency is 500 Hz relative to the original samplerate. So if the filter has been initted with 2X the samplerate then it is tuned down to match that same slowed-down pitch (if the oversampled audio was played-back at the original samplerate).
Therefore at the last step if you drop half the samples, it gets "speeded back up" to the original 1000 Hz at the original samplerate again.
The following code, in the case of 4X oversample-- At the beginning of the @sample block, the EQ filters were last hit with the previous spl0 and spl1 from the last time the @sample block got called. So on this current entry, we need three new samples in-between the last spl0/spl1 and the current spl0/spl1.
So first we feed the current spl0 into the 0.75 sample delay, to get "one quarter sample past the last spl0". Then we feed that 0.75 delayed sample thru the oversample-tuned EQ filters and discard/ignore the output (but the EQ filters remember their states). Then we feed the current spl0 into the 0.5 sample delay and EQ filter it, and then feed the current spl0 into the 0.25 sample delay and EQ filter it.
Then finally we feed the current spl0/spl1 thru the EQ filters and then keep that result as the final "downsampled" EQ output.
Because everything in the process would be linear except some tendency toward phase clumping in the top octave, I wonder if anything would need to be bandlimited during the up or down sampling? Maybe if the input is bandlimited there isn't much way that aliasing would get added in this kind of scheme.
It probably sucks bad for some reason or the other, because hardly anything in the real world is inexpensive and also good, but dunno. Maybe I'll test it sometime.
Code: Select all
@init
//https://ccrma.stanford.edu/~jos/pasp/First_Order_Allpass_Interpolation.html
//https://www.dsprelated.com/freebooks/pasp/Delay_Line_Interpolation.html
function JOS_AP_FracDly_Init(a_FracSampDly)
(
this.FracSampDly = max(min(a_FracSampDly, 0.99), 0.01);
this.k = (1 - this.FracSampDly) / (1 + this.FracSampDly);
this.p1 = 0.0;
this.last_p1 = 0.0;
);
//only call after filter was created via _Init
function JOS_AP_FracDly_SetFracSampDly(a_FracSampDly)
(
this.FracSampDly = max(min(a_FracSampDly, 0.99), 0.01);
this.k = (1 - this.FracSampDly) / (1 + this.FracSampDly);
);
//only call after filter was created via _Init
//Returns the delayed sample
function JOS_AP_FracDly_DoSamp(a_InSamp)
local (l_result)
(
this.p1 = a_InSamp - (this.k * this.last_p1);
l_result = this.last_p1 + (this.k * this.p1);
this.last_p1 = this.p1;
l_result;
);
//assume that EQ Filter objects have been defined here to your specs
//no specific EQ filter is included in this simple example
//maybe something that works like this:
// PeakingFilter_Init(a_Filt_Freq_Hz, a_Filt_Bandwidth, a_FiltBoostOrCutDB, a_SamplingRate)
// PeakingFilter_Set((a_Filt_Freq_Hz, a_Filt_Bandwidth, a_FiltBoostOrCutDB, a_SamplingRate)
// PeakingFilter_DoSamp(a_InSamp)
(srate >= 176400) ?
g_OvrSmpMul = 1.0
:
(
(srate >= 88200) ?
g_OvrSmpMul = 2.0
:
g_OvrSmpMul = 4.0;
);
//init JOS fractional delay filters
o_JosApDly_075_L.JOS_AP_FracDly_Init(0.75);
o_JosApDly_050_L.JOS_AP_FracDly_Init(0.50);
o_JosApDly_025_L.JOS_AP_FracDly_Init(0.25);
o_JosApDly_075_R.JOS_AP_FracDly_Init(0.75);
o_JosApDly_050_R.JOS_AP_FracDly_Init(0.50);
o_JosApDly_025_R.JOS_AP_FracDly_Init(0.25);
//init fictional crude stereo 2 band EQ
//o_Bass_L.PeakingFilter_Init(80, 2.0, 3, srate * g_OvrSmpMul);
//o_Bass_R.PeakingFilter_Init(80, 2.0, 3, srate * g_OvrSmpMul);
//o_Treble_L.PeakingFilter_Init(5000, 2.0, 3, srate * g_OvrSmpMul);
//o_Treble_R.PeakingFilter_Init(5000, 2.0, 3, srate * g_OvrSmpMul);
@sample
(g_OvrSmpMul >= 4) ? //4X oversample
(
s_Tmp_L = o_JosApDly_075_L.JOS_AP_FracDly_DoSamp(spl0);
//s_Tmp_L = o_Bass_L.PeakingFilter_DoSamp(s_Tmp_L);
//s_Tmp_L = o_Treble_L.PeakingFilter_DoSamp(s_Tmp_L);
s_Tmp_L = o_JosApDly_050_L.JOS_AP_FracDly_DoSamp(spl0);
//s_Tmp_L = o_Bass_L.PeakingFilter_DoSamp(s_Tmp_L);
//s_Tmp_L = o_Treble_L.PeakingFilter_DoSamp(s_Tmp_L);
s_Tmp_L = o_JosApDly_025_L.JOS_AP_FracDly_DoSamp(spl0);
//s_Tmp_L = o_Bass_L.PeakingFilter_DoSamp(s_Tmp_L);
//s_Tmp_L = o_Treble_L.PeakingFilter_DoSamp(s_Tmp_L);
s_Tmp_R = o_JosApDly_075_R.JOS_AP_FracDly_DoSamp(spl1);
//s_Tmp_R = o_Bass_R.PeakingFilter_DoSamp(s_Tmp_R);
//s_Tmp_R = o_Treble_R.PeakingFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_JosApDly_050_R.JOS_AP_FracDly_DoSamp(spl1);
//s_Tmp_R = o_Bass_R.PeakingFilter_DoSamp(s_Tmp_R);
//s_Tmp_R = o_Treble_R.PeakingFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_JosApDly_025_R.JOS_AP_FracDly_DoSamp(spl1);
//s_Tmp_R = o_Bass_R.PeakingFilter_DoSamp(s_Tmp_R);
//s_Tmp_R = o_Treble_R.PeakingFilter_DoSamp(s_Tmp_R);
)
:
(
(g_OvrSmpMul >= 2) ? //2X oversample
(
s_Tmp_L = o_JosApDly_050_L.JOS_AP_FracDly_DoSamp(spl0);
//s_Tmp_L = o_Bass_L.PeakingFilter_DoSamp(s_Tmp_L);
//s_Tmp_L = o_Treble_L.PeakingFilter_DoSamp(s_Tmp_L);
s_Tmp_R = o_JosApDly_050_R.JOS_AP_FracDly_DoSamp(spl1);
//s_Tmp_R = o_Bass_R.PeakingFilter_DoSamp(s_Tmp_R);
//s_Tmp_R = o_Treble_R.PeakingFilter_DoSamp(s_Tmp_R);
);
);
//keep the last time thru the EQ filters and write back to spl0 and spl1
//s_Tmp_L = o_Bass_L.PeakingFilter_DoSamp(spl0);
//spl0 = o_Treble_L.PeakingFilter_DoSamp(s_Tmp_L);
//s_Tmp_R = o_Bass_R.PeakingFilter_DoSamp(spl1);
//spl1 = o_Treble_R.PeakingFilter_DoSamp(s_Tmp_R);