From 247ed8e2110752d41e3b2bee91e2ea07b9ecdb71 Mon Sep 17 00:00:00 2001 From: Andrew Belt Date: Tue, 2 Nov 2021 06:03:39 -0400 Subject: [PATCH] Fix wavetable interpolation for WTVCO. --- src/VCO.cpp | 6 +- src/WTLFO.cpp | 11 ++- src/WTVCO.cpp | 166 ++++++++++++++++++++-------------------------- src/Wavetable.hpp | 43 +++++++----- 4 files changed, 108 insertions(+), 118 deletions(-) diff --git a/src/VCO.cpp b/src/VCO.cpp index b075933..a179332 100644 --- a/src/VCO.cpp +++ b/src/VCO.cpp @@ -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(mm2px(Vec(22.905, 29.808)), module, VCO::FREQ_PARAM)); addParam(createParamCentered(mm2px(Vec(22.862, 56.388)), module, VCO::PW_PARAM)); addParam(createParamCentered(mm2px(Vec(6.607, 80.603)), module, VCO::FM_PARAM)); - addParam(createParamCentered(mm2px(Vec(17.444, 80.603)), module, VCO::LINEAR_PARAM)); - addParam(createParamCentered(mm2px(Vec(28.282, 80.603)), module, VCO::SYNC_PARAM)); + addParam(createParamCentered(mm2px(Vec(17.444, 80.603)), module, VCO::LINEAR_PARAM)); + addParam(createParamCentered(mm2px(Vec(28.282, 80.603)), module, VCO::SYNC_PARAM)); addParam(createParamCentered(mm2px(Vec(39.118, 80.603)), module, VCO::PWM_PARAM)); addInput(createInputCentered(mm2px(Vec(6.607, 96.859)), module, VCO::FM_INPUT)); diff --git a/src/WTLFO.cpp b/src/WTLFO.cpp index bf2085f..c460496 100644 --- a/src/WTLFO.cpp +++ b/src/WTLFO.cpp @@ -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(); diff --git a/src/WTVCO.cpp b/src/WTVCO.cpp index 6b3a4fa..638595e 100644 --- a/src/WTVCO.cpp +++ b/src/WTVCO.cpp @@ -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(); - linear = random::get(); - } - 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(c) + inputs[FM_INPUT].getPolyVoltageSimd(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(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(c) + inputs[FM_INPUT].getPolyVoltageSimd(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(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(mm2px(Vec(8.915, 56.388)), module, WTVCO::FREQ_PARAM)); addParam(createParamCentered(mm2px(Vec(26.645, 56.388)), module, WTVCO::POS_PARAM)); addParam(createParamCentered(mm2px(Vec(6.897, 80.603)), module, WTVCO::FM_PARAM)); - addParam(createLightParamCentered>>(mm2px(Vec(17.734, 80.603)), module, WTVCO::LINEAR_PARAM, WTVCO::LINEAR_LIGHT)); + addParam(createLightParamCentered>>(mm2px(Vec(17.734, 80.603)), module, WTVCO::LINEAR_PARAM, WTVCO::LINEAR_LIGHT)); addParam(createParamCentered(mm2px(Vec(28.571, 80.603)), module, WTVCO::POS_CV_PARAM)); - addParam(createLightParamCentered>>(mm2px(Vec(17.734, 96.859)), module, WTVCO::SOFT_PARAM, WTVCO::SOFT_LIGHT)); + addParam(createLightParamCentered>>(mm2px(Vec(17.734, 96.859)), module, WTVCO::SOFT_PARAM, WTVCO::SOFT_LIGHT)); addInput(createInputCentered(mm2px(Vec(6.897, 96.813)), module, WTVCO::FM_INPUT)); addInput(createInputCentered(mm2px(Vec(28.571, 96.859)), module, WTVCO::POS_INPUT)); diff --git a/src/Wavetable.hpp b/src/Wavetable.hpp index 1a365c9..352a3ae 100644 --- a/src/Wavetable.hpp +++ b/src/Wavetable.hpp @@ -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 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();} ));