Browse Source

Fix wavetable interpolation for WTVCO.

tags/v2.0.1
Andrew Belt 3 years ago
parent
commit
247ed8e211
4 changed files with 108 additions and 118 deletions
  1. +3
    -3
      src/VCO.cpp
  2. +5
    -6
      src/WTLFO.cpp
  3. +73
    -93
      src/WTVCO.cpp
  4. +27
    -16
      src/Wavetable.hpp

+ 3
- 3
src/VCO.cpp View File

@@ -288,7 +288,7 @@ struct VCO : Module {
configParam(FM_PARAM, 0.f, 1.f, 0.f, "Frequency modulation", "%", 0.f, 100.f);
configParam(PW_PARAM, 0.01f, 0.99f, 0.5f, "Pulse width", "%", 0.f, 100.f);
configParam(PWM_PARAM, 0.f, 1.f, 0.f, "Pulse width modulation", "%", 0.f, 100.f);
configInput(PITCH_INPUT, "1V/oct pitch");
configInput(PITCH_INPUT, "1V/octave pitch");
configInput(FM_INPUT, "Frequency modulation");
configInput(SYNC_INPUT, "Sync");
configInput(PW_INPUT, "Pulse width modulation");
@@ -371,8 +371,8 @@ struct VCOWidget : ModuleWidget {
addParam(createParamCentered<RoundHugeBlackKnob>(mm2px(Vec(22.905, 29.808)), module, VCO::FREQ_PARAM));
addParam(createParamCentered<RoundLargeBlackKnob>(mm2px(Vec(22.862, 56.388)), module, VCO::PW_PARAM));
addParam(createParamCentered<Trimpot>(mm2px(Vec(6.607, 80.603)), module, VCO::FM_PARAM));
addParam(createParamCentered<LEDButton>(mm2px(Vec(17.444, 80.603)), module, VCO::LINEAR_PARAM));
addParam(createParamCentered<LEDButton>(mm2px(Vec(28.282, 80.603)), module, VCO::SYNC_PARAM));
addParam(createParamCentered<LEDLatch>(mm2px(Vec(17.444, 80.603)), module, VCO::LINEAR_PARAM));
addParam(createParamCentered<LEDLatch>(mm2px(Vec(28.282, 80.603)), module, VCO::SYNC_PARAM));
addParam(createParamCentered<Trimpot>(mm2px(Vec(39.118, 80.603)), module, VCO::PWM_PARAM));

addInput(createInputCentered<PJ301MPort>(mm2px(Vec(6.607, 96.859)), module, VCO::FM_INPUT));


+ 5
- 6
src/WTLFO.cpp View File

@@ -109,11 +109,13 @@ struct WTLFO : Module {
}

void process(const ProcessArgs& args) override {
float freqParam = params[FREQ_PARAM].getValue();
float fmParam = params[FM_PARAM].getValue();
float posParam = params[POS_PARAM].getValue();
float posCvParam = params[POS_CV_PARAM].getValue();
bool offset = (params[OFFSET_PARAM].getValue() > 0.f);
bool invert = (params[INVERT_PARAM].getValue() > 0.f);

int channels = std::max(1, inputs[FM_INPUT].getChannels());

// Clock
if (inputs[CLOCK_INPUT].isConnected()) {
clockTimer.process(args.sampleTime);
@@ -131,10 +133,7 @@ struct WTLFO : Module {
clockFreq = 2.f;
}

float freqParam = params[FREQ_PARAM].getValue();
float fmParam = params[FM_PARAM].getValue();
float posParam = params[POS_PARAM].getValue();
float posCvParam = params[POS_CV_PARAM].getValue();
int channels = std::max(1, inputs[FM_INPUT].getChannels());

// Check valid wave and wavetable size
int waveCount = wavetable.getWaveCount();


+ 73
- 93
src/WTVCO.cpp View File

@@ -37,8 +37,6 @@ struct WTVCO : Module {
};

Wavetable wavetable;
bool soft = false;
bool linear = false;

float_4 phases[4] = {};
float lastPos = 0.f;
@@ -49,39 +47,37 @@ struct WTVCO : Module {

WTVCO() {
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
configButton(SOFT_PARAM, "Soft sync");
configButton(LINEAR_PARAM, "Linear frequency modulation");

configSwitch(SOFT_PARAM, 0.f, 1.f, 0.f, "Sync", {"Hard", "Soft"});
configSwitch(LINEAR_PARAM, 0.f, 1.f, 0.f, "Linear FM");

configParam(FREQ_PARAM, -75.f, 75.f, 0.f, "Frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4);
configParam(POS_PARAM, 0.f, 1.f, 0.f, "Wavetable position", "%", 0.f, 100.f);
configParam(FM_PARAM, -1.f, 1.f, 0.f, "Frequency modulation", "%", 0.f, 100.f);
getParamQuantity(FM_PARAM)->randomizeEnabled = false;
configParam(POS_CV_PARAM, -1.f, 1.f, 0.f, "Wavetable position CV", "%", 0.f, 100.f);
getParamQuantity(POS_CV_PARAM)->randomizeEnabled = false;

configInput(FM_INPUT, "Frequency modulation");
configInput(SYNC_INPUT, "Sync");
configInput(POS_INPUT, "Wavetable position");
configInput(PITCH_INPUT, "1V/octave pitch");

configOutput(WAVE_OUTPUT, "Wave");

configLight(PHASE_LIGHT, "Phase");

lightDivider.setDivision(16);
wavetable.octaves = 8;
wavetable.setQuality(4);

lightDivider.setDivision(16);

onReset();
}

void onReset() override {
soft = false;
linear = false;
wavetable.reset();
}

void onRandomize() override {
soft = random::get<bool>();
linear = random::get<bool>();
}

void onAdd(const AddEvent& e) override {
std::string path = system::join(getPatchStorageDirectory(), "wavetable.wav");
// Silently fails
@@ -104,80 +100,76 @@ struct WTVCO : Module {
}

void process(const ProcessArgs& args) override {
if (linearTrigger.process(params[LINEAR_PARAM].getValue() > 0.f))
linear ^= true;
if (softTrigger.process(params[SOFT_PARAM].getValue() > 0.f))
soft ^= true;

int channels = std::max({1, inputs[PITCH_INPUT].getChannels(), inputs[FM_INPUT].getChannels()});

float freqParam = params[FREQ_PARAM].getValue();
float fmParam = params[FM_PARAM].getValue();
float posParam = params[POS_PARAM].getValue();
float posCvParam = params[POS_CV_PARAM].getValue();
bool soft = params[SOFT_PARAM].getValue() > 0.f;
bool linear = params[LINEAR_PARAM].getValue() > 0.f;

// Check valid wave and wavetable size
if (wavetable.waveLen < 2) {
clearOutput();
return;
}
int waveCount = wavetable.getWaveCount();
if (waveCount < 1) {
clearOutput();
return;
}
int channels = std::max({1, inputs[PITCH_INPUT].getChannels(), inputs[FM_INPUT].getChannels()});

// Iterate channels
for (int c = 0; c < channels; c += 4) {
// Calculate frequency in Hz
float_4 pitch = freqParam / 12.f + inputs[PITCH_INPUT].getPolyVoltageSimd<float_4>(c) + inputs[FM_INPUT].getPolyVoltageSimd<float_4>(c) * fmParam;
float_4 freq = dsp::FREQ_C4 * simd::pow(2.f, pitch);
// Limit to Nyquist frequency
freq = simd::fmin(freq, args.sampleRate / 2.f);

// Number of octaves above frequency until Nyquist
float_4 octave = simd::log2(args.sampleRate / 2 / freq);

// Accumulate phase
float_4 phase = phases[c / 4];
phase += freq * args.sampleTime;
// Wrap phase
phase -= simd::trunc(phase);
phases[c / 4] = phase;
// Scale phase from 0 to waveLen
phase *= (wavetable.waveLen * wavetable.quality);

// Get wavetable position, scaled from 0 to (waveCount - 1)
float_4 pos = posParam + inputs[POS_INPUT].getPolyVoltageSimd<float_4>(c) * posCvParam / 10.f;
pos = simd::clamp(pos);
pos *= (waveCount - 1);

if (c == 0)
lastPos = pos[0];

float_4 out = 0.f;
for (int cc = 0; cc < 4 && c + cc < channels; cc++) {
// Get wave indexes
float phaseF = phase[cc] - std::trunc(phase[cc]);
size_t i0 = std::trunc(phase[cc]);
size_t i1 = (i0 + 1) % (wavetable.waveLen * wavetable.quality);
// Get pos indexes
float posF = pos[cc] - std::trunc(pos[cc]);
size_t pos0 = std::trunc(pos[cc]);
size_t pos1 = pos0 + 1;
// Get waves
int octave0 = clamp((int) octave[cc], 0, 7);
float out0 = crossfade(wavetable.interpolatedAt(octave0, pos0, i0), wavetable.interpolatedAt(octave0, pos0, i1), phaseF);
if (posF > 0.f) {
float out1 = crossfade(wavetable.interpolatedAt(octave0, pos1, i0), wavetable.interpolatedAt(octave0, pos1, i1), phaseF);
out[cc] = crossfade(out0, out1, posF);
}
else {
out[cc] = out0;
int waveCount = wavetable.getWaveCount();
if (wavetable.waveLen >= 2 && waveCount >= 1) {
// Iterate channels
for (int c = 0; c < channels; c += 4) {
// Calculate frequency in Hz
float_4 pitch = freqParam / 12.f + inputs[PITCH_INPUT].getPolyVoltageSimd<float_4>(c) + inputs[FM_INPUT].getPolyVoltageSimd<float_4>(c) * fmParam;
float_4 freq = dsp::FREQ_C4 * dsp::approxExp2_taylor5(pitch + 30.f) / std::pow(2.f, 30.f);

// Limit to Nyquist frequency
freq = simd::fmin(freq, args.sampleRate / 2);
float_4 nyquistRatio = args.sampleRate / 2 / freq;

// Accumulate phase
float_4 phase = phases[c / 4];
phase += freq * args.sampleTime;
// Wrap phase
phase -= simd::trunc(phase);
phases[c / 4] = phase;
// Scale phase from 0 to waveLen
phase *= (wavetable.waveLen * wavetable.quality);

// Get wavetable position, scaled from 0 to (waveCount - 1)
float_4 pos = posParam + inputs[POS_INPUT].getPolyVoltageSimd<float_4>(c) * posCvParam / 10.f;
pos = simd::clamp(pos);
pos *= (waveCount - 1);

if (c == 0)
lastPos = pos[0];

float_4 out = 0.f;
for (int cc = 0; cc < 4 && c + cc < channels; cc++) {
// Get wave indexes
float phaseF = phase[cc] - std::trunc(phase[cc]);
size_t i0 = std::trunc(phase[cc]);
size_t i1 = (i0 + 1) % (wavetable.waveLen * wavetable.quality);
// Get pos indexes
float posF = pos[cc] - std::trunc(pos[cc]);
size_t pos0 = std::trunc(pos[cc]);
size_t pos1 = pos0 + 1;
// Get waves
// TODO Interpolate octaves
int octave0 = math::log2((int) nyquistRatio[cc]);
octave0 = clamp(octave0, 0, (int) wavetable.octaves - 1);
float out0 = crossfade(wavetable.interpolatedAt(octave0, pos0, i0), wavetable.interpolatedAt(octave0, pos0, i1), phaseF);
if (posF > 0.f) {
float out1 = crossfade(wavetable.interpolatedAt(octave0, pos1, i0), wavetable.interpolatedAt(octave0, pos1, i1), phaseF);
out[cc] = crossfade(out0, out1, posF);
}
else {
out[cc] = out0;
}
}
}

outputs[WAVE_OUTPUT].setVoltageSimd(out * 5.f, c);
outputs[WAVE_OUTPUT].setVoltageSimd(out * 5.f, c);
}
}
else {
// Wavetable is invalid, so set 0V
for (int c = 0; c < channels; c += 4) {
outputs[WAVE_OUTPUT].setVoltageSimd(float_4(0.f), c);
}
}

outputs[WAVE_OUTPUT].setChannels(channels);
@@ -202,10 +194,6 @@ struct WTVCO : Module {

json_t* dataToJson() override {
json_t* rootJ = json_object();
// soft
json_object_set_new(rootJ, "soft", json_boolean(soft));
// linear
json_object_set_new(rootJ, "linear", json_boolean(linear));
// Merge wavetable
json_t* wavetableJ = wavetable.toJson();
json_object_update(rootJ, wavetableJ);
@@ -214,14 +202,6 @@ struct WTVCO : Module {
}

void dataFromJson(json_t* rootJ) override {
// soft
json_t* softJ = json_object_get(rootJ, "soft");
if (softJ)
soft = json_boolean_value(softJ);
// linear
json_t* linearJ = json_object_get(rootJ, "linear");
if (linearJ)
linear = json_boolean_value(linearJ);
// wavetable
wavetable.fromJson(rootJ);
}
@@ -241,9 +221,9 @@ struct WTVCOWidget : ModuleWidget {
addParam(createParamCentered<RoundLargeBlackKnob>(mm2px(Vec(8.915, 56.388)), module, WTVCO::FREQ_PARAM));
addParam(createParamCentered<RoundLargeBlackKnob>(mm2px(Vec(26.645, 56.388)), module, WTVCO::POS_PARAM));
addParam(createParamCentered<Trimpot>(mm2px(Vec(6.897, 80.603)), module, WTVCO::FM_PARAM));
addParam(createLightParamCentered<LEDLightButton<MediumSimpleLight<YellowLight>>>(mm2px(Vec(17.734, 80.603)), module, WTVCO::LINEAR_PARAM, WTVCO::LINEAR_LIGHT));
addParam(createLightParamCentered<LEDLightLatch<MediumSimpleLight<WhiteLight>>>(mm2px(Vec(17.734, 80.603)), module, WTVCO::LINEAR_PARAM, WTVCO::LINEAR_LIGHT));
addParam(createParamCentered<Trimpot>(mm2px(Vec(28.571, 80.603)), module, WTVCO::POS_CV_PARAM));
addParam(createLightParamCentered<LEDLightButton<MediumSimpleLight<YellowLight>>>(mm2px(Vec(17.734, 96.859)), module, WTVCO::SOFT_PARAM, WTVCO::SOFT_LIGHT));
addParam(createLightParamCentered<LEDLightLatch<MediumSimpleLight<WhiteLight>>>(mm2px(Vec(17.734, 96.859)), module, WTVCO::SOFT_PARAM, WTVCO::SOFT_LIGHT));

addInput(createInputCentered<PJ301MPort>(mm2px(Vec(6.897, 96.813)), module, WTVCO::FM_INPUT));
addInput(createInputCentered<PJ301MPort>(mm2px(Vec(28.571, 96.859)), module, WTVCO::POS_INPUT));


+ 27
- 16
src/Wavetable.hpp View File

@@ -21,7 +21,7 @@ struct Wavetable {
// Interpolated wavetables
/** Upsampling factor. No upsampling if 0. */
size_t quality = 0;
/** Number of filtered wavetables to precompute */
/** Number of filtered wavetables. Automatically computed from waveLen. */
size_t octaves = 0;
/** (octave, waveCount, waveLen * quality) */
std::vector<float> interpolatedSamples;
@@ -90,7 +90,7 @@ struct Wavetable {
}

void interpolate() {
if (quality == 0 || octaves == 0)
if (quality == 0)
return;
if (waveLen < 2)
return;
@@ -99,31 +99,38 @@ struct Wavetable {
if (waveCount == 0)
return;

octaves = math::log2(waveLen) - 1;
interpolatedSamples.clear();
interpolatedSamples.resize(octaves * samples.size() * quality);
float* in = new float[quality * waveLen]();
float* inF = new float[2 * quality * waveLen];
dsp::RealFFT fft(quality * waveLen);

float* in = new float[waveLen];
float* inF = new float[2 * waveLen];
dsp::RealFFT inFFT(waveLen);

float* outF = new float[2 * waveLen * quality]();
dsp::RealFFT outFFT(waveLen * quality);

for (size_t i = 0; i < waveCount; i++) {
// Zero-stuff interpolated wave
// Compute FFT of wave
for (size_t j = 0; j < waveLen; j++) {
in[j * quality] = samples[i * waveLen + j] / waveLen;
in[j] = samples[waveLen * i + j] / waveLen;
}
fft.rfft(in, inF);
// Lowpass inF
for (int octave = octaves - 1; octave >= 0; octave--) {
size_t firstJ = 1 << (octave + 1);
for (size_t j = firstJ; j < waveLen * quality; j++) {
inF[2 * j + 0] = 0.f;
inF[2 * j + 1] = 0.f;
inFFT.rfft(in, inF);
// Compute FFT-filtered versions of each wave
for (size_t octave = 0; octave < octaves; octave++) {
size_t bins = 1 << octave;
// Only overwrite the first waveLen bins
for (size_t j = 0; j < waveLen; j++) {
outF[2 * j + 0] = (j <= bins) ? inF[2 * j + 0] : 0.f;
outF[2 * j + 1] = (j <= bins) ? inF[2 * j + 1] : 0.f;
}
fft.irfft(inF, &interpolatedSamples[samples.size() * quality * octave + waveLen * quality * i]);
outFFT.irfft(outF, &interpolatedSamples[samples.size() * quality * octave + waveLen * quality * i]);
}
}

delete[] inF;
delete[] in;
delete[] inF;
delete[] outF;
}

json_t* toJson() const {
@@ -262,6 +269,10 @@ struct Wavetable {
}

void appendContextMenu(Menu* menu) {
menu->addChild(createMenuItem("Initialize wavetable", "",
[=]() {reset();}
));

menu->addChild(createMenuItem("Load wavetable", "",
[=]() {loadDialog();}
));


Loading…
Cancel
Save