diff --git a/plugin.json b/plugin.json index 001182b..5d60bda 100644 --- a/plugin.json +++ b/plugin.json @@ -24,8 +24,8 @@ }, { "slug": "VCO2", - "name": "VCO-2", - "description": "Voltage-controlled oscillator with output morphing", + "name": "Wavetable VCO (beta)", + "description": "Voltage-controlled wavetable oscillator", "manualUrl": "https://vcvrack.com/Fundamental#VCO", "tags": [ "VCO", @@ -65,7 +65,7 @@ }, { "slug": "LFO", - "name": "LFO-1", + "name": "LFO", "description": "Low-frequency oscillator", "manualUrl": "https://vcvrack.com/Fundamental#LFO", "tags": [ diff --git a/src/Delay.cpp b/src/Delay.cpp index 21e5c09..a27785e 100644 --- a/src/Delay.cpp +++ b/src/Delay.cpp @@ -1,5 +1,5 @@ #include "plugin.hpp" -#include "samplerate.h" +#include #define HISTORY_SIZE (1<<21) diff --git a/src/WTLFO.cpp b/src/WTLFO.cpp index 83994bc..f7a9680 100644 --- a/src/WTLFO.cpp +++ b/src/WTLFO.cpp @@ -1,9 +1,5 @@ #include "plugin.hpp" -#include -#include "dr_wav.h" - - -static const char WAVETABLE_FILTERS[] = "WAV (.wav):wav,WAV"; +#include "Wavetable.hpp" using simd::float_4; @@ -39,19 +35,14 @@ struct WTLFO : Module { NUM_LIGHTS }; - // All waves concatenated - std::vector wavetable; - // Number of points in each wave - size_t waveLen = 0; + Wavetable wavetable; bool offset = false; bool invert = false; - std::string filename; float_4 phases[4] = {}; float lastPos = 0.f; float clockFreq = 1.f; dsp::Timer clockTimer; - bool clockEnabled = false; dsp::ClockDivider lightDivider; dsp::BooleanTrigger offsetTrigger; @@ -82,12 +73,14 @@ struct WTLFO : Module { configParam(FREQ_PARAM, -8.f, 10.f, 1.f, "Frequency", " Hz", 2, 1); 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(RESET_INPUT, "Reset"); configInput(POS_INPUT, "Wavetable position"); configInput(CLOCK_INPUT, "Clock"); - configOutput(WAVE_OUTPUT, "Wavetable"); + configOutput(WAVE_OUTPUT, "Wave"); configLight(PHASE_LIGHT, "Phase"); lightDivider.setDivision(16); @@ -96,24 +89,9 @@ struct WTLFO : Module { void onReset(const ResetEvent& e) override { Module::onReset(e); - - // Build geometric waveforms - filename = "Basic.wav"; - wavetable.clear(); - waveLen = 1024; - wavetable.resize(waveLen * 4); - - for (size_t i = 0; i < waveLen; i++) { - float p = float(i) / waveLen; - float sin = std::sin(2 * float(M_PI) * p); - wavetable[i + 0 * waveLen] = sin; - float tri = (p < 0.25f) ? 4*p : (p < 0.75f) ? 2 - 4*p : 4*p - 4; - wavetable[i + 1 * waveLen] = tri; - float saw = (p < 0.5f) ? 2*p : 2*p - 2; - wavetable[i + 2 * waveLen] = saw; - float sqr = (p < 0.5f) ? 1 : -1; - wavetable[i + 3 * waveLen] = sqr; - } + offset = false; + invert = false; + wavetable.reset(); // Reset state for (int c = 0; c < 16; c += 4) { @@ -121,6 +99,25 @@ struct WTLFO : Module { } } + void onRandomize(const RandomizeEvent& e) override { + Module::onRandomize(e); + offset = random::get(); + invert = random::get(); + } + + void onAdd(const AddEvent& e) override { + std::string path = system::join(getPatchStorageDirectory(), "wavetable.wav"); + // Silently fails + wavetable.load(path); + } + + void onSave(const SaveEvent& e) override { + if (!wavetable.samples.empty()) { + std::string path = system::join(createPatchStorageDirectory(), "wavetable.wav"); + wavetable.save(path); + } + } + void clearOutput() { outputs[WAVE_OUTPUT].setVoltage(0.f); outputs[WAVE_OUTPUT].setChannels(1); @@ -130,11 +127,6 @@ 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(); - if (offsetTrigger.process(params[OFFSET_PARAM].getValue() > 0.f)) offset ^= true; if (invertTrigger.process(params[INVERT_PARAM].getValue() > 0.f)) @@ -142,17 +134,6 @@ struct WTLFO : Module { int channels = std::max(1, inputs[FM_INPUT].getChannels()); - // Check valid wave and wavetable size - if (waveLen < 2) { - clearOutput(); - return; - } - int wavetableLen = wavetable.size() / waveLen; - if (wavetableLen < 1) { - clearOutput(); - return; - } - // Clock if (inputs[CLOCK_INPUT].isConnected()) { clockTimer.process(args.sampleTime); @@ -170,6 +151,22 @@ 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(); + + // Check valid wave and wavetable size + if (wavetable.waveLen < 2) { + clearOutput(); + return; + } + int waveCount = wavetable.getWaveCount(); + if (waveCount < 1) { + clearOutput(); + return; + } + // Iterate channels for (int c = 0; c < channels; c += 4) { // Calculate frequency in Hz @@ -187,35 +184,35 @@ struct WTLFO : Module { phase = simd::ifelse(reset, 0.f, phase); phases[c / 4] = phase; // Scale phase from 0 to waveLen - phase *= waveLen; + phase *= wavetable.waveLen; - // Get wavetable position, scaled from 0 to (wavetableLen - 1) + // 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 *= (wavetableLen - 1); + pos *= (waveCount - 1); if (c == 0) lastPos = pos[0]; // Get wavetable points float_4 out = 0.f; - for (int i = 0; i < 4 && c + i < channels; i++) { + for (int cc = 0; cc < 4 && c + cc < channels; cc++) { // Get wave indexes - float phaseF = phase[i] - std::trunc(phase[i]); - size_t i0 = std::trunc(phase[i]); - size_t i1 = (i0 + 1) % waveLen; + float phaseF = phase[cc] - std::trunc(phase[cc]); + size_t i0 = std::trunc(phase[cc]); + size_t i1 = (i0 + 1) % wavetable.waveLen; // Get pos indexes - float posF = pos[0] - std::trunc(pos[0]); - size_t pos0 = std::trunc(pos[0]); + float posF = pos[cc] - std::trunc(pos[cc]); + size_t pos0 = std::trunc(pos[cc]); size_t pos1 = pos0 + 1; // Get waves - float out0 = crossfade(wavetable[i0 + pos0 * waveLen], wavetable[i1 + pos0 * waveLen], phaseF); + float out0 = crossfade(wavetable.at(i0, pos0), wavetable.at(i1, pos0), phaseF); if (posF > 0.f) { - float out1 = crossfade(wavetable[i0 + pos1 * waveLen], wavetable[i1 + pos1 * waveLen], phaseF); - out[i] = crossfade(out0, out1, posF); + float out1 = crossfade(wavetable.at(i0, pos1), wavetable.at(i1, pos1), phaseF); + out[cc] = crossfade(out0, out1, posF); } else { - out[i] = out0; + out[cc] = out0; } } @@ -248,188 +245,30 @@ struct WTLFO : Module { } } - void loadWavetable(std::string path) { - drwav wav; - // TODO Unicode on Windows - if (!drwav_init_file(&wav, path.c_str(), NULL)) - return; - - waveLen = 0; - wavetable.clear(); - wavetable.resize(wav.totalPCMFrameCount * wav.channels); - waveLen = 256; - filename = system::getFilename(path); - - drwav_read_pcm_frames_f32(&wav, wav.totalPCMFrameCount, wavetable.data()); - - drwav_uninit(&wav); + json_t* dataToJson() override { + json_t* rootJ = json_object(); + // offset + json_object_set_new(rootJ, "offset", json_boolean(offset)); + // invert + json_object_set_new(rootJ, "invert", json_boolean(invert)); + // Merge wavetable + json_t* wavetableJ = wavetable.toJson(); + json_object_update(rootJ, wavetableJ); + json_decref(wavetableJ); + return rootJ; } - void loadWavetableDialog() { - osdialog_filters* filters = osdialog_filters_parse(WAVETABLE_FILTERS); - DEFER({osdialog_filters_free(filters);}); - - char* pathC = osdialog_file(OSDIALOG_OPEN, NULL, NULL, filters); - if (!pathC) { - // Fail silently - return; - } - std::string path = pathC; - std::free(pathC); - - loadWavetable(path); - } - - void saveWavetable(std::string path) { - drwav_data_format format; - format.container = drwav_container_riff; - format.format = DR_WAVE_FORMAT_PCM; - format.channels = 1; - format.sampleRate = 44100; - format.bitsPerSample = 16; - - drwav wav; - if (!drwav_init_file_write(&wav, path.c_str(), &format, NULL)) - return; - - size_t len = wavetable.size(); - int16_t* buf = new int16_t[len]; - drwav_f32_to_s16(buf, wavetable.data(), len); - drwav_write_pcm_frames(&wav, len, buf); - delete[] buf; - - drwav_uninit(&wav); - } - - void saveWavetableDialog() { - osdialog_filters* filters = osdialog_filters_parse(WAVETABLE_FILTERS); - DEFER({osdialog_filters_free(filters);}); - - char* pathC = osdialog_file(OSDIALOG_SAVE, NULL, filename.c_str(), filters); - if (!pathC) { - // Cancel silently - return; - } - DEFER({std::free(pathC);}); - - // Automatically append .wav extension - std::string path = pathC; - if (system::getExtension(path) != ".wav") { - path += ".wav"; - } - - saveWavetable(path); - } -}; - - -static std::vector sineWavetable; - -static void sineWavetableInit() { - sineWavetable.clear(); - size_t len = 128; - sineWavetable.resize(len); - for (size_t i = 0; i < len; i++) { - float p = float(i) / len; - sineWavetable[i] = std::sin(2 * float(M_PI) * p); - } -} - - -template -struct WTDisplay : LedDisplay { - TModule* module; - - void drawLayer(const DrawArgs& args, int layer) override { - if (layer == 1) { - // Lazily initialize default wavetable for display - if (sineWavetable.empty()) - sineWavetableInit(); - - // Get module data or defaults - const std::vector& wavetable = module ? module->wavetable : sineWavetable; - size_t waveLen = module ? module->waveLen : sineWavetable.size(); - float lastPos = module ? module->lastPos : 0.f; - std::string filename = module ? module->filename : "Basic.wav"; - - // Draw filename text - std::shared_ptr font = APP->window->loadFont(asset::system("res/fonts/ShareTechMono-Regular.ttf")); - nvgFontSize(args.vg, 13); - nvgFontFaceId(args.vg, font->handle); - nvgTextLetterSpacing(args.vg, -2); - nvgFillColor(args.vg, SCHEME_YELLOW); - nvgText(args.vg, 4.0, 13.0, filename.c_str(), NULL); - - // Get wavetable metadata - if (waveLen < 2) - return; - - size_t wavetableLen = wavetable.size() / waveLen; - if (wavetableLen < 1) - return; - if (lastPos > wavetableLen - 1) - return; - float posF = lastPos - std::trunc(lastPos); - size_t pos0 = std::trunc(lastPos); - - // Draw scope - nvgScissor(args.vg, RECT_ARGS(args.clipBox)); - nvgBeginPath(args.vg); - Vec scopePos = Vec(0.0, 13.0); - Rect scopeRect = Rect(scopePos, box.size - scopePos); - scopeRect = scopeRect.shrink(Vec(4, 5)); - size_t iSkip = waveLen / 128 + 1; - - for (size_t i = 0; i <= waveLen; i += iSkip) { - // Get wave value - float wave; - float wave0 = wavetable[(i % waveLen) + waveLen * pos0]; - if (posF > 0.f) { - float wave1 = wavetable[(i % waveLen) + waveLen * (pos0 + 1)]; - wave = crossfade(wave0, wave1, posF); - } - else { - wave = wave0; - } - - // Add point to line - Vec p; - p.x = float(i) / waveLen; - p.y = 0.5f - 0.5f * wave; - p = scopeRect.pos + scopeRect.size * p; - if (i == 0) - nvgMoveTo(args.vg, VEC_ARGS(p)); - else - nvgLineTo(args.vg, VEC_ARGS(p)); - } - nvgLineCap(args.vg, NVG_ROUND); - nvgMiterLimit(args.vg, 2.f); - nvgStrokeWidth(args.vg, 1.5f); - nvgStrokeColor(args.vg, SCHEME_YELLOW); - nvgStroke(args.vg); - } - LedDisplay::drawLayer(args, layer); - } - - void onButton(const ButtonEvent& e) override { - if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT) { - if (module) - module->loadWavetableDialog(); - e.consume(this); - } - LedDisplay::onButton(e); - } - - void onPathDrop(const PathDropEvent& e) override { - if (!module) - return; - if (e.paths.empty()) - return; - std::string path = e.paths[0]; - if (system::getExtension(path) != ".wav") - return; - module->loadWavetable(path); - e.consume(this); + void dataFromJson(json_t* rootJ) override { + // offset + json_t* offsetJ = json_object_get(rootJ, "offset"); + if (offsetJ) + offset = json_boolean_value(offsetJ); + // invert + json_t* invertJ = json_object_get(rootJ, "invert"); + if (invertJ) + invert = json_boolean_value(invertJ); + // wavetable + wavetable.fromJson(rootJ); } }; @@ -472,23 +311,7 @@ struct WTLFOWidget : ModuleWidget { menu->addChild(new MenuSeparator); - menu->addChild(createMenuItem("Load wavetable", "", - [=]() {module->loadWavetableDialog();} - )); - - menu->addChild(createMenuItem("Save wavetable", "", - [=]() {module->saveWavetableDialog();} - )); - - int sizeOffset = 4; - std::vector sizeLabels; - for (int i = sizeOffset; i <= 14; i++) { - sizeLabels.push_back(string::f("%d", 1 << i)); - } - menu->addChild(createIndexSubmenuItem("Wave points", sizeLabels, - [=]() {return math::log2(module->waveLen) - sizeOffset;}, - [=](int i) {module->waveLen = 1 << (i + sizeOffset);} - )); + module->wavetable.appendContextMenu(menu); } }; diff --git a/src/WTVCO.cpp b/src/WTVCO.cpp index 4a5b75a..edce617 100644 --- a/src/WTVCO.cpp +++ b/src/WTVCO.cpp @@ -1,103 +1,261 @@ #include "plugin.hpp" +#include "Wavetable.hpp" +#include + + +using simd::float_4; struct WTVCO : Module { enum ParamIds { MODE_PARAM, // removed - SYNC_PARAM, + SOFT_PARAM, FREQ_PARAM, - WAVE_PARAM, + POS_PARAM, FM_PARAM, + // added in 2.0 + POS_CV_PARAM, + LINEAR_PARAM, NUM_PARAMS }; enum InputIds { FM_INPUT, SYNC_INPUT, - WAVE_INPUT, + POS_INPUT, + // added in 2.0 + PITCH_INPUT, NUM_INPUTS }; enum OutputIds { - OUT_OUTPUT, + WAVE_OUTPUT, NUM_OUTPUTS }; enum LightIds { ENUMS(PHASE_LIGHT, 3), + SOFT_LIGHT, + LINEAR_LIGHT, NUM_LIGHTS }; - // VoltageControlledOscillator<8, 8, float_4> oscillators[4]; + Wavetable wavetable; + bool soft = false; + bool linear = false; + + float lastPos = 0.f; + SRC_STATE* src[16]; + // callback state + uint8_t callbackChannel = 0; + float callbackPos = 0.f; + uint32_t callbackIndexes[16] = {}; + static constexpr int CALLBACK_BUFFER_LEN = 16; + float callbackBuffers[16][CALLBACK_BUFFER_LEN] = {}; + dsp::ClockDivider lightDivider; + dsp::BooleanTrigger softTrigger; + dsp::BooleanTrigger linearTrigger; WTVCO() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); - configSwitch(MODE_PARAM, 0.f, 1.f, 1.f, "Engine mode", {"Digital", "Analog"}); - configSwitch(SYNC_PARAM, 0.f, 1.f, 1.f, "Sync mode", {"Soft", "Hard"}); - configParam(FREQ_PARAM, -54.f, 54.f, 0.f, "Frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4); - configParam(WAVE_PARAM, 0.f, 3.f, 1.5f, "Wave"); - configParam(FM_PARAM, 0.f, 1.f, 1.f, "Frequency modulation", "%", 0.f, 100.f); + configButton(SOFT_PARAM, "Soft sync"); + configButton(LINEAR_PARAM, "Linear frequency modulation"); + 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(WAVE_INPUT, "Wave type"); - configOutput(OUT_OUTPUT, "Audio"); + configInput(POS_INPUT, "Wavetable position"); + configInput(PITCH_INPUT, "1V/octave pitch"); + configOutput(WAVE_OUTPUT, "Wave"); + configLight(PHASE_LIGHT, "Phase"); lightDivider.setDivision(16); + + for (int c = 0; c < 16; c++) { + src[c] = src_callback_new(srcCallback, SRC_SINC_FASTEST, 1, NULL, this); + assert(src[c]); + } + + onReset(ResetEvent()); + } + + ~WTVCO() { + for (int c = 0; c < 16; c++) { + src_delete(src[c]); + } + } + + void onReset(const ResetEvent& e) override { + Module::onReset(e); + soft = false; + linear = false; + wavetable.reset(); + } + + void onRandomize(const RandomizeEvent& e) override { + Module::onRandomize(e); + soft = random::get(); + linear = random::get(); + } + + void onAdd(const AddEvent& e) override { + std::string path = system::join(getPatchStorageDirectory(), "wavetable.wav"); + // Silently fails + wavetable.load(path); + } + + void onSave(const SaveEvent& e) override { + if (!wavetable.samples.empty()) { + std::string path = system::join(createPatchStorageDirectory(), "wavetable.wav"); + wavetable.save(path); + } + } + + void clearOutput() { + outputs[WAVE_OUTPUT].setVoltage(0.f); + outputs[WAVE_OUTPUT].setChannels(1); + lights[PHASE_LIGHT + 0].setBrightness(0.f); + lights[PHASE_LIGHT + 1].setBrightness(0.f); + lights[PHASE_LIGHT + 2].setBrightness(0.f); + } + + static long srcCallback(void* cbData, float** data) { + WTVCO* that = (WTVCO*) cbData; + int c = that->callbackChannel; + // Get pos + float posF = that->callbackPos - std::trunc(that->callbackPos); + size_t pos0 = std::trunc(that->callbackPos); + size_t pos1 = pos0 + 1; + // Fill callbackBuffer + for (int i = 0; i < CALLBACK_BUFFER_LEN; i++) { + size_t index = (that->callbackIndexes[c] + i) % that->wavetable.waveLen; + // Get waves + float out; + float out0 = that->wavetable.at(index, pos0); + if (posF > 0.f) { + float out1 = that->wavetable.at(index, pos1); + out = crossfade(out0, out1, posF); + } + else { + out = out0; + } + + that->callbackBuffers[c][i] = out; + } + + that->callbackIndexes[c] += CALLBACK_BUFFER_LEN; + data[0] = that->callbackBuffers[c]; + return CALLBACK_BUFFER_LEN; + } + + void fillInputBuffer(int c) { } void process(const ProcessArgs& args) override { - // float freqParam = params[FREQ_PARAM].getValue() / 12.f; - // float fmParam = dsp::quadraticBipolar(params[FM_PARAM].getValue()); - // float waveParam = params[WAVE_PARAM].getValue(); - - // int channels = std::max(inputs[FM_INPUT].getChannels(), 1); - - // for (int c = 0; c < channels; c += 4) { - // auto* oscillator = &oscillators[c / 4]; - // oscillator->channels = std::min(channels - c, 4); - // oscillator->analog = (params[MODE_PARAM].getValue() > 0.f); - // oscillator->soft = (params[SYNC_PARAM].getValue() <= 0.f); - - // float_4 pitch = freqParam; - // pitch += fmParam * inputs[FM_INPUT].getVoltageSimd(c); - // oscillator->setPitch(pitch); - - // oscillator->syncEnabled = inputs[SYNC_INPUT].isConnected(); - // oscillator->process(args.sampleTime, inputs[SYNC_INPUT].getPolyVoltageSimd(c)); - - // // Outputs - // if (outputs[OUT_OUTPUT].isConnected()) { - // float_4 wave = simd::clamp(waveParam + inputs[WAVE_INPUT].getPolyVoltageSimd(c) / 10.f * 3.f, 0.f, 3.f); - // float_4 v = 0.f; - // v += oscillator->sin() * simd::fmax(0.f, 1.f - simd::fabs(wave - 0.f)); - // v += oscillator->tri() * simd::fmax(0.f, 1.f - simd::fabs(wave - 1.f)); - // v += oscillator->saw() * simd::fmax(0.f, 1.f - simd::fabs(wave - 2.f)); - // v += oscillator->sqr() * simd::fmax(0.f, 1.f - simd::fabs(wave - 3.f)); - // outputs[OUT_OUTPUT].setVoltageSimd(5.f * v, c); - // } - // } - - // outputs[OUT_OUTPUT].setChannels(channels); - - // // Light - // if (lightDivider.process()) { - // if (channels == 1) { - // float lightValue = oscillators[0].light()[0]; - // lights[PHASE_LIGHT + 0].setSmoothBrightness(-lightValue, args.sampleTime * lightDivider.getDivision()); - // lights[PHASE_LIGHT + 1].setSmoothBrightness(lightValue, args.sampleTime * lightDivider.getDivision()); - // lights[PHASE_LIGHT + 2].setBrightness(0.f); - // } - // else { - // lights[PHASE_LIGHT + 0].setBrightness(0.f); - // lights[PHASE_LIGHT + 1].setBrightness(0.f); - // lights[PHASE_LIGHT + 2].setBrightness(1.f); - // } - // } + 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(); + + // Check valid wave and wavetable size + if (wavetable.waveLen < 2) { + clearOutput(); + return; + } + int waveCount = wavetable.getWaveCount(); + if (waveCount < 1) { + clearOutput(); + return; + } + + // 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); + + float_4 ratio = args.sampleRate / freq / wavetable.waveLen; + + // 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++) { + callbackChannel = c + cc; + callbackPos = pos[cc]; + // Not sure why this call is needed since we set ratio in src_callback_read(). Perhaps a SRC bug? + src_set_ratio(src[c + cc], ratio[cc]); + float outBuf[1]; + long ret = src_callback_read(src[c + cc], ratio[cc], 1, outBuf); + // DEBUG("ret %ld ratio %f", ret, ratio[cc]); + if (ret > 0) + out[cc] = outBuf[0]; + } + + outputs[WAVE_OUTPUT].setVoltageSimd(out * 5.f, c); + } + + outputs[WAVE_OUTPUT].setChannels(channels); + + // Light + if (lightDivider.process()) { + if (channels == 1) { + // float b = 1.f - phases[0][0]; + // lights[PHASE_LIGHT + 0].setSmoothBrightness(b, args.sampleTime * lightDivider.getDivision()); + // lights[PHASE_LIGHT + 1].setBrightness(0.f); + // lights[PHASE_LIGHT + 2].setBrightness(0.f); + } + else { + lights[PHASE_LIGHT + 0].setBrightness(0.f); + lights[PHASE_LIGHT + 1].setBrightness(0.f); + lights[PHASE_LIGHT + 2].setBrightness(1.f); + } + lights[LINEAR_LIGHT].setBrightness(linear); + lights[SOFT_LIGHT].setBrightness(soft); + } } -}; + 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); + json_decref(wavetableJ); + return rootJ; + } -struct WTVCODisplay : LedDisplay { - WTVCODisplay() { - box.size = mm2px(Vec(35.56, 29.021)); + 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); } }; @@ -113,22 +271,34 @@ struct WTVCOWidget : ModuleWidget { addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addParam(createParamCentered(mm2px(Vec(8.915, 56.388)), module, WTVCO::FREQ_PARAM)); - addParam(createParamCentered(mm2px(Vec(26.645, 56.388)), module, WTVCO::WAVE_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(createParamCentered(mm2px(Vec(17.734, 80.603)), module, WTVCO::LFM_PARAM)); - // addParam(createParamCentered(mm2px(Vec(28.571, 80.603)), module, WTVCO::POSCV_PARAM)); - addParam(createParamCentered(mm2px(Vec(17.734, 96.859)), module, WTVCO::SYNC_PARAM)); + 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)); addInput(createInputCentered(mm2px(Vec(6.897, 96.813)), module, WTVCO::FM_INPUT)); - addInput(createInputCentered(mm2px(Vec(28.571, 96.859)), module, WTVCO::WAVE_INPUT)); - // addInput(createInputCentered(mm2px(Vec(6.897, 113.115)), module, WTVCO::PITCH_INPUT)); + addInput(createInputCentered(mm2px(Vec(28.571, 96.859)), module, WTVCO::POS_INPUT)); + addInput(createInputCentered(mm2px(Vec(6.897, 113.115)), module, WTVCO::PITCH_INPUT)); addInput(createInputCentered(mm2px(Vec(17.734, 113.115)), module, WTVCO::SYNC_INPUT)); - addOutput(createOutputCentered(mm2px(Vec(28.571, 113.115)), module, WTVCO::OUT_OUTPUT)); + addOutput(createOutputCentered(mm2px(Vec(28.571, 113.115)), module, WTVCO::WAVE_OUTPUT)); addChild(createLightCentered>(mm2px(Vec(17.733, 49.409)), module, WTVCO::PHASE_LIGHT)); - addChild(createWidget(mm2px(Vec(0.0, 13.039)))); + WTDisplay* display = createWidget>(mm2px(Vec(0.004, 13.04))); + display->box.size = mm2px(Vec(35.56, 29.224)); + display->module = module; + addChild(display); + } + + void appendContextMenu(Menu* menu) override { + WTVCO* module = dynamic_cast(this->module); + assert(module); + + menu->addChild(new MenuSeparator); + + module->wavetable.appendContextMenu(menu); } }; diff --git a/src/Wavetable.hpp b/src/Wavetable.hpp new file mode 100644 index 0000000..6761ef1 --- /dev/null +++ b/src/Wavetable.hpp @@ -0,0 +1,260 @@ +#pragma once +#include +#include +#include "dr_wav.h" + + +static const char WAVETABLE_FILTERS[] = "WAV (.wav):wav,WAV"; + + +/** Loads and stores wavetable samples and metadata */ +struct Wavetable { + /** All waves concatenated */ + std::vector samples; + /** Number of points in each wave */ + size_t waveLen = 0; + /** Name of loaded wavetable. */ + std::string filename; + + Wavetable() { + reset(); + } + + float &at(size_t sampleIndex, size_t waveIndex) { + return samples[sampleIndex + waveIndex * waveLen]; + } + float at(size_t sampleIndex, size_t waveIndex) const { + return samples[sampleIndex + waveIndex * waveLen]; + } + + /** Returns the number of waves in the wavetable. */ + size_t getWaveCount() const { + return samples.size() / waveLen; + } + + void reset() { + filename = "Basic.wav"; + waveLen = 256; + samples.clear(); + samples.resize(waveLen * 4); + + for (size_t i = 0; i < waveLen; i++) { + float p = float(i) / waveLen; + float sin = std::sin(2 * float(M_PI) * p); + at(i, 0) = sin; + float tri = (p < 0.25f) ? 4*p : (p < 0.75f) ? 2 - 4*p : 4*p - 4; + at(i, 1) = tri; + float saw = (p < 0.5f) ? 2*p : 2*p - 2; + at(i, 2) = saw; + float sqr = (p < 0.5f) ? 1 : -1; + at(i, 3) = sqr; + } + } + + json_t* toJson() const { + json_t* rootJ = json_object(); + // waveLen + json_object_set_new(rootJ, "waveLen", json_integer(waveLen)); + // filename + json_object_set_new(rootJ, "filename", json_string(filename.c_str())); + return rootJ; + } + + void fromJson(json_t* rootJ) { + // waveLen + json_t* waveLenJ = json_object_get(rootJ, "waveLen"); + if (waveLenJ) + waveLen = json_integer_value(waveLenJ); + // filename + json_t* filenameJ = json_object_get(rootJ, "filename"); + if (filenameJ) + filename = json_string_value(filenameJ); + } + + void load(std::string path) { + drwav wav; + // TODO Unicode on Windows + if (!drwav_init_file(&wav, path.c_str(), NULL)) + return; + + samples.clear(); + samples.resize(wav.totalPCMFrameCount * wav.channels); + + drwav_read_pcm_frames_f32(&wav, wav.totalPCMFrameCount, samples.data()); + + drwav_uninit(&wav); + } + + void loadDialog() { + osdialog_filters* filters = osdialog_filters_parse(WAVETABLE_FILTERS); + DEFER({osdialog_filters_free(filters);}); + + char* pathC = osdialog_file(OSDIALOG_OPEN, NULL, NULL, filters); + if (!pathC) { + // Fail silently + return; + } + std::string path = pathC; + std::free(pathC); + + load(path); + filename = system::getFilename(path); + } + + void save(std::string path) const { + drwav_data_format format; + format.container = drwav_container_riff; + format.format = DR_WAVE_FORMAT_PCM; + format.channels = 1; + format.sampleRate = 44100; + format.bitsPerSample = 16; + + drwav wav; + if (!drwav_init_file_write(&wav, path.c_str(), &format, NULL)) + return; + + size_t len = samples.size(); + int16_t* buf = new int16_t[len]; + drwav_f32_to_s16(buf, samples.data(), len); + drwav_write_pcm_frames(&wav, len, buf); + delete[] buf; + + drwav_uninit(&wav); + } + + void saveDialog() const { + osdialog_filters* filters = osdialog_filters_parse(WAVETABLE_FILTERS); + DEFER({osdialog_filters_free(filters);}); + + char* pathC = osdialog_file(OSDIALOG_SAVE, NULL, filename.c_str(), filters); + if (!pathC) { + // Cancel silently + return; + } + DEFER({std::free(pathC);}); + + // Automatically append .wav extension + std::string path = pathC; + if (system::getExtension(path) != ".wav") { + path += ".wav"; + } + + save(path); + } + + void appendContextMenu(Menu* menu) { + menu->addChild(createMenuItem("Load wavetable", "", + [=]() {loadDialog();} + )); + + menu->addChild(createMenuItem("Save wavetable", "", + [=]() {saveDialog();} + )); + + int sizeOffset = 4; + std::vector sizeLabels; + for (int i = sizeOffset; i <= 14; i++) { + sizeLabels.push_back(string::f("%d", 1 << i)); + } + menu->addChild(createIndexSubmenuItem("Wave points", sizeLabels, + [=]() {return math::log2(waveLen) - sizeOffset;}, + [=](int i) {waveLen = 1 << (i + sizeOffset);} + )); + } +}; + + +static const Wavetable defaultWavetable; + + +template +struct WTDisplay : LedDisplay { + TModule* module; + + void drawLayer(const DrawArgs& args, int layer) override { + if (layer == 1) { + // Get module data or defaults + const Wavetable& wavetable = module ? module->wavetable : defaultWavetable; + float lastPos = module ? module->lastPos : 0.f; + + // Draw filename text + std::shared_ptr font = APP->window->loadFont(asset::system("res/fonts/ShareTechMono-Regular.ttf")); + nvgFontSize(args.vg, 13); + nvgFontFaceId(args.vg, font->handle); + nvgFillColor(args.vg, SCHEME_YELLOW); + nvgText(args.vg, 4.0, 13.0, wavetable.filename.c_str(), NULL); + + // Get wavetable metadata + if (wavetable.waveLen < 2) + return; + + size_t waveCount = wavetable.getWaveCount(); + if (waveCount < 1) + return; + if (lastPos > waveCount - 1) + return; + float posF = lastPos - std::trunc(lastPos); + size_t pos0 = std::trunc(lastPos); + + // Draw scope + nvgScissor(args.vg, RECT_ARGS(args.clipBox)); + nvgBeginPath(args.vg); + Vec scopePos = Vec(0.0, 13.0); + Rect scopeRect = Rect(scopePos, box.size - scopePos); + scopeRect = scopeRect.shrink(Vec(4, 5)); + size_t iSkip = wavetable.waveLen / 128 + 1; + + for (size_t i = 0; i <= wavetable.waveLen; i += iSkip) { + // Get wave value + float wave; + float wave0 = wavetable.at(i % wavetable.waveLen, pos0); + if (posF > 0.f) { + float wave1 = wavetable.at(i % wavetable.waveLen, (pos0 + 1)); + wave = crossfade(wave0, wave1, posF); + } + else { + wave = wave0; + } + + // Add point to line + Vec p; + p.x = float(i) / wavetable.waveLen; + p.y = 0.5f - 0.5f * wave; + p = scopeRect.pos + scopeRect.size * p; + if (i == 0) + nvgMoveTo(args.vg, VEC_ARGS(p)); + else + nvgLineTo(args.vg, VEC_ARGS(p)); + } + nvgLineCap(args.vg, NVG_ROUND); + nvgMiterLimit(args.vg, 2.f); + nvgStrokeWidth(args.vg, 1.5f); + nvgStrokeColor(args.vg, SCHEME_YELLOW); + nvgStroke(args.vg); + } + LedDisplay::drawLayer(args, layer); + } + + // void onButton(const ButtonEvent& e) override { + // if (e.action == GLFW_PRESS && e.button == GLFW_MOUSE_BUTTON_LEFT) { + // if (module) + // module->loadWavetableDialog(); + // e.consume(this); + // } + // LedDisplay::onButton(e); + // } + + void onPathDrop(const PathDropEvent& e) override { + if (!module) + return; + if (e.paths.empty()) + return; + std::string path = e.paths[0]; + if (system::getExtension(path) != ".wav") + return; + module->wavetable.load(path); + module->wavetable.filename = system::getFilename(path); + e.consume(this); + } +}; + diff --git a/src/plugin.hpp b/src/plugin.hpp index 7940556..d5dd055 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -1,4 +1,4 @@ -#include "rack.hpp" +#include using namespace rack;