From f283cd671b10c6cd6d3a2a39c34c3fb454a64a50 Mon Sep 17 00:00:00 2001 From: hemmer <915048+hemmer@users.noreply.github.com> Date: Sun, 21 May 2023 18:57:43 +0100 Subject: [PATCH] Initial work to make PonyVCO poly See #43 --- src/ChowDSP.hpp | 55 +++++---- src/PonyVCO.cpp | 319 +++++++++++++++++++++++------------------------- 2 files changed, 184 insertions(+), 190 deletions(-) diff --git a/src/ChowDSP.hpp b/src/ChowDSP.hpp index 873a4d9..4d7cd6d 100644 --- a/src/ChowDSP.hpp +++ b/src/ChowDSP.hpp @@ -225,7 +225,7 @@ typedef TBiquadFilter<> BiquadFilter; Currently uses an 2*N-th order Butterworth filter. source: https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/AAFilter.hpp */ -template +template class AAFilter { public: AAFilter() = default; @@ -255,10 +255,10 @@ public: auto Qs = calculateButterQs(2 * N); for (int i = 0; i < N; ++i) - filters[i].setParameters(BiquadFilter::Type::LOWPASS, fc / (osRatio * sampleRate), Qs[i], 1.0f); + filters[i].setParameters(TBiquadFilter::Type::LOWPASS, fc / (osRatio * sampleRate), Qs[i], 1.0f); } - inline float process(float x) noexcept { + inline T process(T x) noexcept { for (int i = 0; i < N; ++i) x = filters[i].process(x); @@ -266,14 +266,16 @@ public: } private: - BiquadFilter filters[N]; + TBiquadFilter filters[N]; }; + /** * Base class for oversampling of any order * source: https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/oversampling.hpp */ +template class BaseOversampling { public: BaseOversampling() = default; @@ -283,13 +285,13 @@ public: virtual void reset(float /*baseSampleRate*/) = 0; /** Upsample a single input sample and update the oversampled buffer */ - virtual void upsample(float) noexcept = 0; + virtual void upsample(T) noexcept = 0; /** Output a downsampled output sample from the current oversampled buffer */ - virtual float downsample() noexcept = 0; + virtual T downsample() noexcept = 0; /** Returns a pointer to the oversampled buffer */ - virtual float* getOSBuffer() noexcept = 0; + virtual T* getOSBuffer() noexcept = 0; }; @@ -305,8 +307,8 @@ public: float y = oversample.downsample(); @endcode */ -template -class Oversampling : public BaseOversampling { +template +class Oversampling : public BaseOversampling { public: Oversampling() = default; virtual ~Oversampling() {} @@ -317,7 +319,7 @@ public: std::fill(osBuffer, &osBuffer[ratio], 0.0f); } - inline void upsample(float x) noexcept override { + inline void upsample(T x) noexcept override { osBuffer[0] = ratio * x; std::fill(&osBuffer[1], &osBuffer[ratio], 0.0f); @@ -325,25 +327,26 @@ public: osBuffer[k] = aiFilter.process(osBuffer[k]); } - inline float downsample() noexcept override { - float y = 0.0f; + inline T downsample() noexcept override { + T y = 0.0f; for (int k = 0; k < ratio; k++) y = aaFilter.process(osBuffer[k]); return y; } - inline float* getOSBuffer() noexcept override { + inline T* getOSBuffer() noexcept override { return osBuffer; } - float osBuffer[ratio]; + T osBuffer[ratio]; private: - AAFilter aaFilter; // anti-aliasing filter - AAFilter aiFilter; // anti-imaging filter + AAFilter aaFilter; // anti-aliasing filter + AAFilter aiFilter; // anti-imaging filter }; +typedef Oversampling<1, 4, simd::float_4> OversamplingSIMD; /** @@ -362,7 +365,7 @@ private: source (modified): https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/VariableOversampling.hpp */ -template +template class VariableOversampling { public: VariableOversampling() = default; @@ -384,17 +387,17 @@ public: } /** Upsample a single input sample and update the oversampled buffer */ - inline void upsample(float x) noexcept { + inline void upsample(T x) noexcept { oss[osIdx]->upsample(x); } /** Output a downsampled output sample from the current oversampled buffer */ - inline float downsample() noexcept { + inline T downsample() noexcept { return oss[osIdx]->downsample(); } /** Returns a pointer to the oversampled buffer */ - inline float* getOSBuffer() noexcept { + inline T* getOSBuffer() noexcept { return oss[osIdx]->getOSBuffer(); } @@ -411,12 +414,12 @@ private: int osIdx = 0; - Oversampling < 1 << 0, filtN > os0; // 1x - Oversampling < 1 << 1, filtN > os1; // 2x - Oversampling < 1 << 2, filtN > os2; // 4x - Oversampling < 1 << 3, filtN > os3; // 8x - Oversampling < 1 << 4, filtN > os4; // 16x - BaseOversampling* oss[NumOS] = { &os0, &os1, &os2, &os3, &os4 }; + Oversampling < 1 << 0, filtN, T > os0; // 1x + Oversampling < 1 << 1, filtN, T > os1; // 2x + Oversampling < 1 << 2, filtN, T > os2; // 4x + Oversampling < 1 << 3, filtN, T > os3; // 8x + Oversampling < 1 << 4, filtN, T > os4; // 16x + BaseOversampling* oss[NumOS] = { &os0, &os1, &os2, &os3, &os4 }; }; } // namespace chowdsp diff --git a/src/PonyVCO.cpp b/src/PonyVCO.cpp index bc424b5..c4a06c4 100644 --- a/src/PonyVCO.cpp +++ b/src/PonyVCO.cpp @@ -1,6 +1,7 @@ #include "plugin.hpp" #include "ChowDSP.hpp" +using simd::float_4; // references: // * "REDUCING THE ALIASING OF NONLINEAR WAVESHAPING USING CONTINUOUS-TIME CONVOLUTION" (https://www.dafx.de/paper-archive/2016/dafxpapers/20-DAFx-16_paper_41-PN.pdf) @@ -8,46 +9,27 @@ // * https://ccrma.stanford.edu/~jatin/Notebooks/adaa.html // * Pony waveshape https://www.desmos.com/calculator/1kvahyl4ti +template class FoldStage1 { public: - float process(float x, float xt) { - float y; + T process(T x, T xt) { + T y = simd::ifelse(simd::abs(x - xPrev) < 1e-5, + f(0.5 * (xPrev + x), xt), + (F(x, xt) - F(xPrev, xt)) / (x - xPrev)); - if (fabs(x - xPrev) < 1e-5) { - y = f(0.5 * (xPrev + x), xt); - } - else { - y = (F(x, xt) - F(xPrev, xt)) / (x - xPrev); - } xPrev = x; return y; } // xt - threshold x - static float f(float x, float xt) { - if (x > xt) { - return +5 * xt - 4 * x; - } - else if (x < -xt) { - return -5 * xt - 4 * x; - } - else { - return x; - } + static T f(T x, T xt) { + return simd::ifelse(x > xt, +5 * xt - 4 * x, simd::ifelse(x < -xt, -5 * xt - 4 * x, x)); } - static float F(float x, float xt) { - if (x > xt) { - return 5 * xt * x - 2 * x * x - 2.5 * xt * xt; - } - else if (x < -xt) { - return -5 * xt * x - 2 * x * x - 2.5 * xt * xt; - - } - else { - return x * x / 2.f; - } + static T F(T x, T xt) { + return simd::ifelse(x > xt, 5 * xt * x - 2 * x * x - 2.5 * xt * xt, + simd::ifelse(x < -xt, -5 * xt * x - 2 * x * x - 2.5 * xt * xt, x * x / 2.f)); } void reset() { @@ -55,55 +37,29 @@ public: } private: - float xPrev = 0.f; + T xPrev = 0.f; }; +template class FoldStage2 { public: - float process(float x) { - float y; - - if (fabs(x - xPrev) < 1e-5) { - y = f(0.5 * (xPrev + x)); - } - else { - y = (F(x) - F(xPrev)) / (x - xPrev); - } + T process(T x) { + const T y = simd::ifelse(simd::abs(x - xPrev) < 1e-5, f(0.5 * (xPrev + x)), (F(x) - F(xPrev)) / (x - xPrev)); xPrev = x; return y; } - static float f(float x) { - if (-(x + 2) > c) { - return c; - } - else if (x < -1) { - return -(x + 2); - } - else if (x < 1) { - return x; - } - else if (-x + 2 > -c) { - return -x + 2; - } - else { - return -c; - } + static T f(T x) { + return simd::ifelse(-(x + 2) > c, c, simd::ifelse(x < -1, -(x + 2), simd::ifelse(x < 1, x, simd::ifelse(-x + 2 > -c, -x + 2, -c)))); } - static float F(float x) { - if (x < 0) { - return F(-x); - } - else if (x < 1) { - return x * x * 0.5; - } - else if (x < 2 + c) { - return 2 * x * (1.f - x * 0.25f) - 1.f; - } - else { - return 2 * (2 + c) * (1 - (2 + c) * 0.25f) - 1.f - c * (x - 2 - c); - } + static T F(T x) { + return simd::ifelse(x > 0, F_signed(x), F_signed(-x)); + } + + static T F_signed(T x) { + return simd::ifelse(x < 1, x * x * 0.5, simd::ifelse(x < 2.f + c, 2.f * x * (1.f - x * 0.25f) - 1.f, + 2.f * (2.f + c) * (1.f - (2.f + c) * 0.25f) - 1.f - c * (x - 2.f - c))); } void reset() { @@ -111,8 +67,8 @@ public: } private: - float xPrev = 0.f; - static constexpr float c = 0.1; + T xPrev = 0.f; + static constexpr float c = 0.1f; }; @@ -148,10 +104,10 @@ struct PonyVCO : Module { }; float range[4] = {8.f, 1.f, 1.f / 12.f, 10.f}; - chowdsp::VariableOversampling<6> oversampler; // uses a 2*6=12th order Butterworth filter + chowdsp::VariableOversampling<6, float_4> oversampler[4]; // uses a 2*6=12th order Butterworth filter int oversamplingIndex = 1; // default is 2^oversamplingIndex == x2 oversampling - dsp::RCFilter blockTZFMDCFilter; + dsp::TRCFilter blockTZFMDCFilter[4]; bool blockTZFMDC = true; // hardware doesn't limit PW but some user might want to (to 5%->95%) @@ -160,10 +116,10 @@ struct PonyVCO : Module { // hardware has DC for non-50% duty cycle, optionally add/remove it bool removePulseDC = true; - dsp::SchmittTrigger syncTrigger; + dsp::TSchmittTrigger syncTrigger[4]; - FoldStage1 stage1; - FoldStage2 stage2; + FoldStage1 stage1[4]; + FoldStage2 stage2[4]; PonyVCO() { config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN); @@ -191,22 +147,21 @@ struct PonyVCO : Module { void onSampleRateChange() override { float sampleRate = APP->engine->getSampleRate(); - blockTZFMDCFilter.setCutoffFreq(5.0 / sampleRate); - oversampler.setOversamplingIndex(oversamplingIndex); - oversampler.reset(sampleRate); + for (int c = 0; c < 4; c++) { + blockTZFMDCFilter[c].setCutoffFreq(5.0 / sampleRate); + oversampler[c].setOversamplingIndex(oversamplingIndex); + oversampler[c].reset(sampleRate); - stage1.reset(); - stage2.reset(); + stage1[c].reset(); + stage2[c].reset(); + } } // implementation taken from "Alias-Suppressed Oscillators Based on Differentiated Polynomial Waveforms", // also the notes from Surge Synthesier repo: // https://github.com/surge-synthesizer/surge/blob/09f1ec8e103265bef6fc0d8a0fc188238197bf8c/src/common/dsp/oscillators/ModernOscillator.cpp#L19 - // Calculation is performed at double precision, as the differencing equations appeared to work poorly with only float. - double phase = 0.0; // phase at current (sub)sample - double phases[3] = {}; // phase as extrapolated to the current and two previous samples - double sawBuffer[3] = {}, sawOffsetBuff[3] = {}, triBuffer[3] = {}; // buffers for storing the terms in the difference equation + float_4 phase[4] = {}; // phase at current (sub)sample void process(const ProcessArgs& args) override { @@ -216,130 +171,161 @@ struct PonyVCO : Module { const Waveform waveform = (Waveform) params[WAVE_PARAM].getValue(); const float mult = lfoMode ? 1.0 : dsp::FREQ_C4; const float baseFreq = std::pow(2, (int)(params[OCT_PARAM].getValue() - 3)) * mult; - const int oversamplingRatio = lfoMode ? 1 : oversampler.getOversamplingRatio(); - const float timbre = clamp(params[TIMBRE_PARAM].getValue() + inputs[TIMBRE_INPUT].getVoltage() / 10.f, 0.f, 1.f); - - float tzfmVoltage = inputs[TZFM_INPUT].getVoltage(); - if (blockTZFMDC) { - blockTZFMDCFilter.process(tzfmVoltage); - tzfmVoltage = blockTZFMDCFilter.highpass(); - } + const int oversamplingRatio = lfoMode ? 1 : oversampler[0].getOversamplingRatio(); - const double pitch = inputs[VOCT_INPUT].getVoltage() + params[FREQ_PARAM].getValue() * range[rangeIndex]; - const double freq = baseFreq * simd::pow(2.f, pitch); - const double deltaBasePhase = clamp(freq * args.sampleTime / oversamplingRatio, -0.5f, 0.5f); - // denominator for the second-order FD - const double denominator = 0.25 / (deltaBasePhase * deltaBasePhase); - // not clamped, but _total_ phase treated later with floor/ceil - const double deltaFMPhase = freq * tzfmVoltage * args.sampleTime / oversamplingRatio; - - float pw = timbre; - if (limitPW) { - pw = clamp(pw, 0.05, 0.95); - } - // pulsewave waveform doesn't have DC even for non 50% duty cycles, but Befaco team would like the option - // for it to be added back in for hardware compatibility reasons - const float pulseDCOffset = (!removePulseDC) * 2.f * (0.5f - pw); - - // hard sync - if (syncTrigger.process(inputs[SYNC_INPUT].getVoltage())) { - // hardware waveform is actually cos, so pi/2 phase offset is required - // - variable phase is defined on [0, 1] rather than [0, 2pi] so pi/2 -> 0.25 - phase = (waveform == WAVE_SIN) ? 0.25f : 0.f; - } + // number of active polyphony engines (must be at least 1) + const int channels = std::max({inputs[TZFM_INPUT].getChannels(), inputs[VOCT_INPUT].getChannels(), inputs[TIMBRE_INPUT].getChannels(), 1}); - float* osBuffer = oversampler.getOSBuffer(); - for (int i = 0; i < oversamplingRatio; ++i) { + for (int c = 0; c < channels; c += 4) { + const float_4 timbre = simd::clamp(params[TIMBRE_PARAM].getValue() + inputs[TIMBRE_INPUT].getPolyVoltageSimd(c) / 10.f, 0.f, 1.f); - phase += deltaBasePhase + deltaFMPhase; - if (phase > 1.f) { - phase -= floor(phase); + float_4 tzfmVoltage = inputs[TZFM_INPUT].getPolyVoltageSimd(c); + if (blockTZFMDC) { + blockTZFMDCFilter[c / 4].process(tzfmVoltage); + tzfmVoltage = blockTZFMDCFilter[c / 4].highpass(); } - else if (phase < 0.f) { - phase += -ceil(phase) + 1; + + const float_4 pitch = inputs[VOCT_INPUT].getPolyVoltageSimd(c) + params[FREQ_PARAM].getValue() * range[rangeIndex]; + const float_4 freq = baseFreq * simd::pow(2.f, pitch); + const float_4 deltaBasePhase = simd::clamp(freq * args.sampleTime / oversamplingRatio, -0.5f, 0.5f); + // floating point arithmetic doesn't work well at low frequencies, specifically because the finite difference denominator + // becomes tiny - we check for that scenario and use naive / 1st order waveforms in that frequency regime (as aliasing isn't + // a problem there). With no oversampling, at 44100Hz, the threshold frequency is 44.1Hz. + const float_4 lowFreqRegime = simd::abs(deltaBasePhase) < 1e-3; + + // 1 / denominator for the second-order FD + const float_4 denominatorInv = 0.25 / (deltaBasePhase * deltaBasePhase); + // not clamped, but _total_ phase treated later with floor/ceil + const float_4 deltaFMPhase = freq * tzfmVoltage * args.sampleTime / oversamplingRatio; + + float_4 pw = timbre; + if (limitPW) { + pw = clamp(pw, 0.05, 0.95); } + // pulsewave waveform doesn't have DC even for non 50% duty cycles, but Befaco team would like the option + // for it to be added back in for hardware compatibility reasons + const float_4 pulseDCOffset = (!removePulseDC) * 2.f * (0.5f - pw); - // sin is simple + // hard sync + const float_4 syncMask = syncTrigger[c / 4].process(inputs[SYNC_INPUT].getPolyVoltageSimd(c)); if (waveform == WAVE_SIN) { - osBuffer[i] = sin2pi_pade_05_5_4(phase); + // hardware waveform is actually cos, so pi/2 phase offset is required + // - variable phase is defined on [0, 1] rather than [0, 2pi] so pi/2 -> 0.25 + phase[c / 4] = simd::ifelse(syncMask, 0.25f, phase[c / 4]); } else { + phase[c / 4] = simd::ifelse(syncMask, 0.f, phase[c / 4]); + } - phases[0] = phase - 2 * deltaBasePhase + (phase < 2 * deltaBasePhase); - phases[1] = phase - deltaBasePhase + (phase < deltaBasePhase); - phases[2] = phase; + float_4* osBuffer = oversampler[c / 4].getOSBuffer(); + for (int i = 0; i < oversamplingRatio; ++i) { - switch (waveform) { - case WAVE_TRI: { - osBuffer[i] = aliasSuppressedTri() * denominator; - break; - } - case WAVE_SAW: { - osBuffer[i] = aliasSuppressedSaw() * denominator; - break; - } - case WAVE_PULSE: { - double saw = aliasSuppressedSaw(); - double sawOffset = aliasSuppressedOffsetSaw(pw); + phase[c / 4] += deltaBasePhase + deltaFMPhase; + // ensure within [0, 1] + phase[c / 4] -= simd::floor(phase[c / 4]); - osBuffer[i] = (sawOffset - saw) * denominator; - osBuffer[i] += pulseDCOffset; - break; + // sin is simple + if (waveform == WAVE_SIN) { + osBuffer[i] = sin2pi_pade_05_5_4(phase[c / 4]); + } + else { + float_4 phases[3]; // phase as extrapolated to the current and two previous samples + + phases[0] = phase[c / 4] - 2 * deltaBasePhase + simd::ifelse(phase[c / 4] < 2 * deltaBasePhase, 1.f, 0.f); + phases[1] = phase[c / 4] - deltaBasePhase + simd::ifelse(phase[c / 4] < deltaBasePhase, 1.f, 0.f); + phases[2] = phase[c / 4]; + + switch (waveform) { + case WAVE_TRI: { + const float_4 dpwOrder1 = 1.0 - 2.0 * simd::abs(2 * phase[c / 4] - 1.0); + const float_4 dpwOrder3 = aliasSuppressedTri(phases) * denominatorInv; + + osBuffer[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3); + break; + } + case WAVE_SAW: { + const float_4 dpwOrder1 = 2 * phase[c / 4] - 1.0; + const float_4 dpwOrder3 = aliasSuppressedSaw(phases) * denominatorInv; + + osBuffer[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3); + break; + } + case WAVE_PULSE: { + + float_4 dpwOrder1 = simd::ifelse(phase[c / 4] < pw, -1.0, +1.0); + dpwOrder1 -= simd::ifelse(removePulseDC, 2.f * (0.5f - pw), 0.f); + + float_4 saw = aliasSuppressedSaw(phases); + float_4 sawOffset = aliasSuppressedOffsetSaw(phases, pw); + float_4 dpwOrder3 = (sawOffset - saw) * denominatorInv + pulseDCOffset; + + osBuffer[i] = simd::ifelse(lowFreqRegime, dpwOrder1, dpwOrder3); + break; + } + default: break; } - default: break; } - } - if (waveform != WAVE_PULSE) { - osBuffer[i] = wavefolder(osBuffer[i], (1 - 0.85 * timbre)); - } - } + if (waveform != WAVE_PULSE) { + osBuffer[i] = wavefolder(osBuffer[i], (1 - 0.85 * timbre), c); + } + + } // end of oversampling loop - // downsample (if required) - const float out = (oversamplingRatio > 1) ? oversampler.downsample() : osBuffer[0]; + // downsample (if required) + const float_4 out = (oversamplingRatio > 1) ? oversampler[c / 4].downsample() : osBuffer[0]; - // end of chain VCA - const float gain = std::max(0.f, inputs[VCA_INPUT].getNormalVoltage(10.f) / 10.f); - outputs[OUT_OUTPUT].setVoltage(5.f * out * gain); + // end of chain VCA + const float_4 gain = simd::clamp(inputs[VCA_INPUT].getNormalPolyVoltageSimd(10.f, c) / 10.f, 0.f, 1.f); + outputs[OUT_OUTPUT].setVoltageSimd(5.f * out * gain, c); + + } // end of channels loop + + outputs[OUT_OUTPUT].setChannels(channels); } - double aliasSuppressedTri() { + float_4 aliasSuppressedTri(float_4* phases) { + float_4 triBuffer[3]; for (int i = 0; i < 3; ++i) { - double p = 2 * phases[i] - 1.0; // range -1.0 to +1.0 - double s = 0.5 - std::abs(p); // eq 30 + float_4 p = 2 * phases[i] - 1.0; // range -1.0 to +1.0 + float_4 s = 0.5 - simd::abs(p); // eq 30 triBuffer[i] = (s * s * s - 0.75 * s) / 3.0; // eq 29 } return (triBuffer[0] - 2.0 * triBuffer[1] + triBuffer[2]); } - double aliasSuppressedSaw() { + float_4 aliasSuppressedSaw(float_4* phases) { + float_4 sawBuffer[3]; for (int i = 0; i < 3; ++i) { - double p = 2 * phases[i] - 1.0; // range -1 to +1 + float_4 p = 2 * phases[i] - 1.0; // range -1 to +1 sawBuffer[i] = (p * p * p - p) / 6.0; // eq 11 } return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]); } - double aliasSuppressedOffsetSaw(double pw) { + float_4 aliasSuppressedOffsetSaw(float_4* phases, float_4 pw) { + float_4 sawOffsetBuff[3]; + for (int i = 0; i < 3; ++i) { - double p = 2 * phases[i] - 1.0; // range -1 to +1 - double pwp = p + 2 * pw; // phase after pw (pw in [0, 1]) - pwp += (pwp > 1) * -2; // modulo on [-1, +1] + float_4 p = 2 * phases[i] - 1.0; // range -1 to +1 + float_4 pwp = p + 2 * pw; // phase after pw (pw in [0, 1]) + pwp += simd::ifelse(pwp > 1, -2, 0); // modulo on [-1, +1] sawOffsetBuff[i] = (pwp * pwp * pwp - pwp) / 6.0; // eq 11 } return (sawOffsetBuff[0] - 2.0 * sawOffsetBuff[1] + sawOffsetBuff[2]); } - float wavefolder(float x, float xt) { - return stage2.process(stage1.process(x, xt)); + float_4 wavefolder(float_4 x, float_4 xt, int c) { + return stage2[c / 4].process(stage1[c / 4].process(x, xt)); } json_t* dataToJson() override { json_t* rootJ = json_object(); json_object_set_new(rootJ, "blockTZFMDC", json_boolean(blockTZFMDC)); json_object_set_new(rootJ, "removePulseDC", json_boolean(removePulseDC)); - json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler.getOversamplingIndex())); + json_object_set_new(rootJ, "limitPW", json_boolean(limitPW)); + json_object_set_new(rootJ, "oversamplingIndex", json_integer(oversampler[0].getOversamplingIndex())); return rootJ; } @@ -355,6 +341,11 @@ struct PonyVCO : Module { removePulseDC = json_boolean_value(removePulseDCJ); } + json_t* limitPWJ = json_object_get(rootJ, "limitPW"); + if (limitPWJ) { + limitPW = json_boolean_value(limitPWJ); + } + json_t* oversamplingIndexJ = json_object_get(rootJ, "oversamplingIndex"); if (oversamplingIndexJ) { oversamplingIndex = json_integer_value(oversamplingIndexJ);