The attached jsfx code should run ok if pasted into a new reaper FX instance.
Because the upsampling is several one-pole IIR allpass in parallel, one can't simply upsample then pick off 1 out of N samples and measure for frequency response and aliasing.
For instance, if 4 X oversampling, the 0.75 sample delayed branch would have flat freq response and no aliasing. And the 0.5 sample delayed branch would have flat freq response and no aliasing. And the same for the 0.25 sample delayed branch and the non-delayed branch.
Therefore to get it to "show bad warts" the branches need to be interleaved together and fed serially thru an IIR filter network of some kind, and then pick out 1 out of 4 of the IIR filter network outputs, without any antialias filtering, to measure what this silly abuse would do to the frequency response and aliasing.
The test project code can be easily modified. As written, it has a quick'n'dirty sine sweep generator that runs from 20 Hz to 20 kHz, about 1 second per octave, and holds about 20 kHz for 1 second at the end before shutting off. So if you put the plugin on an empty reaper track, hilite about an 11 second region from the beginning and render to disk, you get a stereo test output file.
I am running Reaper at 44.1 k samplerate so it is running the test filter network at 4X oversample in my testing so far.
The left stereo output channel is the raw output from the quick'n'dirty sine sweep generator, peaking at -6 dB FS. TURN DOWN YER SPEAKERS IF YER EARS OR TWEETERS ARE FRAGILE!! YOU HAVE BEEN WARNED!!
The right stereo output channel oversamples 4X by interleaving the sine sweep samples with three JOS allpass IIR delays set for 0.75, 0.5, and 0.25 sample delays. For the "test filter network" I feed the 4X stream thru 4 instances of Vadim's first-order ZDF allpass filter, all of them tuned to 1 kHz. Then 1 out of 4 sample outputs from this four-filter cascade is written to the right stereo output, to accomplish a crude nasty downsample with no antialias filtering at all. Just discarding 3 out of 4 filter output samples.
I picked four poles of allpass filter for the "EQ filter stand-in" because it ought to have flat frequency response though of course with all that phase shifting the right channel won't null against the un-processed left channel.
Another way to test might be something like put a non-oversampled lowpass, shelf or peaking filter on the left not-oversampled output and put the same-tuned oversampled lowpass, shelf or peaking filter on the right oversampled output. However that would not null either, even if working perfectly, because of BLT frequency warping. Regardless whether the test filters would be tuned high or low, the oversampled filter network would have "less warped" response than the non-oversampled filter network.
So anyway I'm not trying to "prove a point" just honestly wondering how to realistically evaluate this silly way to oversample. I could try tuning the cascade of allpass filters to something like 100 Hz or 10000 Hz to see if changing the time constants changes the behavior.
However, with the four-pole allpass cascade tuned to 1 kHz, analyzing in Cool Edit Pro looking at the spectral window display, I'm not seeing any obvious increase of aliasing on the oversampled and crudely decimated right channel. The left unprocessed and right processed low level harmonics are about the same level at all frequencies 20 to 20 kHz. Even at 20 kHz the loudest low-level "noise" harmonics are no more than -115 dB below the frequency peak. Maybe some other tool would show different results.
However, the crude up/downsampled processed file shows a little bit of gain starting around 10 kHz and rising up to about +2.5 dB gain at 20 kHz. Which is surprising because I would have expected a little bit of FIR lowpass filtering, maybe a few dB of loss in the top octave. I had originally suspected maybe the gain is some kind of symptom of aliasing, though the spectral display doesn't show aliasing splatter. Though maybe in this test the aliasing "just happens to reflect back to the same frequency" or something? Dunno.
I can post screen shots or the test WAV if somebody wants to see them. A two-tone intermodulation test might be something useful. Maybe this silly oversampling method would exaggerate intermodulation, dunno.
Maybe there are better ways to show the warts. Maybe this particular test is somehow too unfairly sympathetic.
Code: Select all
desc:TestAllpassUpsample
// James Chandler Jr
// http://www.errnum.com
// License: GPL - http://www.gnu.org/licenses/gpl.html
@init
//should only call js_malloc within js @init section
JS_MALLOC_NO_CLEARMEM = 0;
JS_MALLOC_CLEARMEM = 1;
g_next_js_malloc = 0;
function js_malloc(a_size, a_clearmem)
local (l_result)
(
l_result = g_next_js_malloc;
(a_clearmem != 0) ?
memset(l_result, 0, a_size);
g_next_js_malloc += a_size;
l_result;
);
//initialize sin lookup table
SIN_TBL_SIZE = 4096;
SIN_TBL = js_malloc(SIN_TBL_SIZE + 1, JS_MALLOC_CLEARMEM);
TWO_PI = 2 * $pi;
_phaseinc = TWO_PI / 4096;
_phase = _phaseinc;
SIN_TBL[0] = 0;
SIN_TBL[4096] = 0;
//SIN_TBL[0] = 0 and also SIN_TBL[4096] = 0 to ease phase wrapping interpolation
_i = 1;
while (_i < 4096) //fill out elements [1] thru [4095]
(
SIN_TBL[_i] = sin(_phase);
_i += 1;
_phase += _phaseinc;
);
//function SinSweep_Init(a_BeginFreq_Hz, a_EndFreq_Hz, a_DurationSecs, a_PeakAmplitude)
function SinSweep_Init() //20 to 20 kHz 1 octave per second
(
this.AmpMul = 0.5;
this.BeginFreq_Hz = 20;
this.BeginFreq_SinTblStepsPerSec = this.BeginFreq_Hz * SIN_TBL_SIZE;
this.BeginFreq_SinTblIncPerSample = this.BeginFreq_SinTblStepsPerSec / srate;
this.EndFreq_Hz = 20000;
this.EndFreq_SinTblStepsPerSec = this.EndFreq_Hz * SIN_TBL_SIZE;
this.EndFreq_SinTblIncPerSample = this.EndFreq_SinTblStepsPerSec / srate;
this.OneOctavePerSecIncMul = 2 ^ (1 / srate);
this.SinTblPhaseIdx = 0;
this.SinTblIncPerSample = this.BeginFreq_SinTblIncPerSample;
this.EndFreqPlaySampleCount = 0; //play the top 20 kHz for 1 second before outputting silence
);
function SinSweep_DoSamp()
local (l_result, l_int, l_frac)
(
l_int = floor(this.SinTblPhaseIdx);
l_frac = this.SinTblPhaseIdx - l_int;
l_result = SIN_TBL[l_int] * (1 - l_frac) + SIN_TBL[l_int + 1] * l_frac; //linear interpolate
this.SinTblPhaseIdx += this.SinTblIncPerSample;
(this.SinTblPhaseIdx >= SIN_TBL_SIZE) ? //phase wrap
this.SinTblPhaseIdx -= SIN_TBL_SIZE;
(this.SinTblIncPerSample < this.EndFreq_SinTblIncPerSample) ? //gradually inc freq up to 20 kHz
this.SinTblIncPerSample *= this.OneOctavePerSecIncMul
:
(
(this.EndFreqPlaySampleCount < srate) ?
this.EndFreqPlaySampleCount += 1
:
l_result = 0; //silent after 1 second of 20 kHz, don't risk frying deaf people's tweeters
);
l_result *= this.AmpMul;
);
//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;
);
//First order trapezoidal filter object
//Code adapted from Vadim Zavalishin's book "The Art of VA Filter Design"
FIRST_ORD_FILTTYPE_LOPASS = 0;
FIRST_ORD_FILTTYPE_HIPASS = 1;
FIRST_ORD_FILTTYPE_ALLPASS_ADV = 2; //+180 degrees phase shift at DC, descending to 0 degrees phase shift at nyquist
FIRST_ORD_FILTTYPE_ALLPASS_RET = 3; //0 degrees phase shift at DC, descending to -180 degrees phase shift at nyquist
//FiltTyps: Use one of the above to set the return value of FirstOrdTrapezoidFilter_DoSamp()
// : However, all values are simultaneously accessible after calling DoSamp() by reading
// : TheFilter.lp, TheFilter.hp, TheFilter.ap_A, TheFilter.ap_R
//a_FiltFC: Filter center frequency in Hz
//a_SampRate: Samplerate of filter
function FirstOrdTrapezoidFilter_Init(a_FiltType, a_FiltFC, a_SampRate)
(
this.FT = a_FiltType;
this.SR = a_SampRate;
this.Nyquist = floor(this.SR * 0.5);
this.FC = min(a_FiltFC, this.Nyquist - 1);
this.s = 0.0;
this.lp = 0;
this.hp = 0;
this.ap_A = 0;
this.ap_R = 0;
//calculate coefficient
this.g = tan($pi * this.FC / this.SR);
this.g /= (1 + this.g);
);
function FirstOrdTrapezoidFilter_SetFC(a_FiltFC)
(
this.FC = min(a_FiltFC, this.Nyquist - 1);
this.g = tan($pi * this.FC / this.SR);
this.g /= (1 + this.g);
);
//Returns the filtered sample
function FirstOrdTrapezoidFilter_DoSamp(a_InSamp)
local (l_v, l_result)
(
//Vadim Zavalishin code
//v = (x-z1_state)*g/(1+g);
//y = v + z1_state;
//z1_state = y + v;
l_v = (a_InSamp - this.s) * this.g;
this.lp = l_v + this.s;
this.s = this.lp + l_v;
this.hp = a_InSamp - this.lp;
this.ap_A = this.hp - this.lp;
this.ap_R = this.lp - this.hp;
(this.FT == FIRST_ORD_FILTTYPE_LOPASS) ?
l_result = this.lp
:
(this.FT == FIRST_ORD_FILTTYPE_HIPASS) ?
l_result = this.hp
:
(this.FT == FIRST_ORD_FILTTYPE_ALLPASS_ADV) ?
l_result = this.ap_A
:
l_result = this.ap_R;
l_result;
);
(srate >= 176400) ?
g_OvrSmpMul = 1.0
:
(
(srate >= 88200) ?
g_OvrSmpMul = 2.0
:
g_OvrSmpMul = 4.0;
);
o_SineSweeper.SinSweep_Init();
//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 test first order allpass filters
o_TestAllpassFilt_1_R.FirstOrdTrapezoidFilter_Init(FIRST_ORD_FILTTYPE_ALLPASS_RET, 1000, srate * g_OvrSmpMul);
o_TestAllpassFilt_2_R.FirstOrdTrapezoidFilter_Init(FIRST_ORD_FILTTYPE_ALLPASS_RET, 1000, srate * g_OvrSmpMul);
o_TestAllpassFilt_3_R.FirstOrdTrapezoidFilter_Init(FIRST_ORD_FILTTYPE_ALLPASS_RET, 1000, srate * g_OvrSmpMul);
o_TestAllpassFilt_4_R.FirstOrdTrapezoidFilter_Init(FIRST_ORD_FILTTYPE_ALLPASS_RET, 1000, srate * g_OvrSmpMul);
@sample
(play_state == 1) ? //if playing
(
s_Tmp_Sine = o_SineSweeper.SinSweep_DoSamp();
(g_OvrSmpMul >= 4) ? //4X oversample
(
//s_Tmp_L = o_JosApDly_075_L.JOS_AP_FracDly_DoSamp(s_Tmp_Sine);
//s_Tmp_L = o_JosApDly_050_L.JOS_AP_FracDly_DoSamp(s_Tmp_Sine);
//s_Tmp_L = o_JosApDly_025_L.JOS_AP_FracDly_DoSamp(s_Tmp_Sine);
s_Tmp_R = o_JosApDly_075_R.JOS_AP_FracDly_DoSamp(s_Tmp_Sine);
s_Tmp_R = o_TestAllpassFilt_1_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_TestAllpassFilt_2_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_TestAllpassFilt_3_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_TestAllpassFilt_4_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_JosApDly_050_R.JOS_AP_FracDly_DoSamp(s_Tmp_Sine);
s_Tmp_R = o_TestAllpassFilt_1_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_TestAllpassFilt_2_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_TestAllpassFilt_3_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_TestAllpassFilt_4_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_JosApDly_025_R.JOS_AP_FracDly_DoSamp(s_Tmp_Sine);
s_Tmp_R = o_TestAllpassFilt_1_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_TestAllpassFilt_2_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_TestAllpassFilt_3_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_TestAllpassFilt_4_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
)
:
(
(g_OvrSmpMul >= 2) ? //2X oversample
(
//s_Tmp_L = o_JosApDly_050_L.JOS_AP_FracDly_DoSamp(s_Tmp_Sine);
s_Tmp_R = o_JosApDly_050_R.JOS_AP_FracDly_DoSamp(s_Tmp_Sine);
s_Tmp_R = o_TestAllpassFilt_1_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_TestAllpassFilt_2_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_TestAllpassFilt_3_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_TestAllpassFilt_4_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
);
);
//left side output is original sine sweep
spl0 = s_Tmp_Sine;
//right side output is "crude decimated" oversampled filter by taking the output
// of the last pass thru the allpass filter cascade
s_Tmp_R = o_TestAllpassFilt_1_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_Sine);
s_Tmp_R = o_TestAllpassFilt_2_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
s_Tmp_R = o_TestAllpassFilt_3_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
spl1 = o_TestAllpassFilt_4_R.FirstOrdTrapezoidFilter_DoSamp(s_Tmp_R);
)
: //else if not playing, output silence
(
spl0 = 0;
spl1 = 0;
);