Browse Source

Initial work to make PonyVCO poly

See #43
pull/46/head
hemmer 1 year ago
parent
commit
f283cd671b
2 changed files with 184 additions and 190 deletions
  1. +29
    -26
      src/ChowDSP.hpp
  2. +155
    -164
      src/PonyVCO.cpp

+ 29
- 26
src/ChowDSP.hpp View File

@@ -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<int N>
template<int N, typename T>
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<T>::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<T> filters[N];
};
/**
* Base class for oversampling of any order
* source: https://github.com/jatinchowdhury18/ChowDSP-VCV/blob/master/src/shared/oversampling.hpp
*/
template<typename T>
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<int ratio, int filtN = 4>
class Oversampling : public BaseOversampling {
template<int ratio, int filtN = 4, typename T = float>
class Oversampling : public BaseOversampling<T> {
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<filtN> aaFilter; // anti-aliasing filter
AAFilter<filtN> aiFilter; // anti-imaging filter
AAFilter<filtN, T> aaFilter; // anti-aliasing filter
AAFilter<filtN, T> 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<int filtN = 4>
template<int filtN = 4, typename T = float>
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<T>* oss[NumOS] = { &os0, &os1, &os2, &os3, &os4 };
};
} // namespace chowdsp

+ 155
- 164
src/PonyVCO.cpp View File

@@ -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<typename T>
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<typename T>
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<float_4> 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<float_4> syncTrigger[4];

FoldStage1 stage1;
FoldStage2 stage2;
FoldStage1<float_4> stage1[4];
FoldStage2<float_4> 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<float_4>(c) / 10.f, 0.f, 1.f);

phase += deltaBasePhase + deltaFMPhase;
if (phase > 1.f) {
phase -= floor(phase);
float_4 tzfmVoltage = inputs[TZFM_INPUT].getPolyVoltageSimd<float_4>(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<float_4>(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<float_4>(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<float_4>(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);


Loading…
Cancel
Save