| @@ -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": [ | |||
| @@ -1,5 +1,5 @@ | |||
| #include "plugin.hpp" | |||
| #include "samplerate.h" | |||
| #include <samplerate.h> | |||
| #define HISTORY_SIZE (1<<21) | |||
| @@ -1,9 +1,5 @@ | |||
| #include "plugin.hpp" | |||
| #include <osdialog.h> | |||
| #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<float> 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<FrequencyQuantity>(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<bool>(); | |||
| invert = random::get<bool>(); | |||
| } | |||
| 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<float_4>(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<float> 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 <class TModule> | |||
| 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<float>& 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> 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<std::string> 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); | |||
| } | |||
| }; | |||
| @@ -1,103 +1,261 @@ | |||
| #include "plugin.hpp" | |||
| #include "Wavetable.hpp" | |||
| #include <samplerate.h> | |||
| 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<bool>(); | |||
| linear = random::get<bool>(); | |||
| } | |||
| 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<float_4>(c); | |||
| // oscillator->setPitch(pitch); | |||
| // oscillator->syncEnabled = inputs[SYNC_INPUT].isConnected(); | |||
| // oscillator->process(args.sampleTime, inputs[SYNC_INPUT].getPolyVoltageSimd<float_4>(c)); | |||
| // // Outputs | |||
| // if (outputs[OUT_OUTPUT].isConnected()) { | |||
| // float_4 wave = simd::clamp(waveParam + inputs[WAVE_INPUT].getPolyVoltageSimd<float_4>(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<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); | |||
| 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<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++) { | |||
| 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<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | |||
| addParam(createParamCentered<RoundLargeBlackKnob>(mm2px(Vec(8.915, 56.388)), module, WTVCO::FREQ_PARAM)); | |||
| addParam(createParamCentered<RoundLargeBlackKnob>(mm2px(Vec(26.645, 56.388)), module, WTVCO::WAVE_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(createParamCentered<LEDButton>(mm2px(Vec(17.734, 80.603)), module, WTVCO::LFM_PARAM)); | |||
| // addParam(createParamCentered<Trimpot>(mm2px(Vec(28.571, 80.603)), module, WTVCO::POSCV_PARAM)); | |||
| addParam(createParamCentered<LEDButton>(mm2px(Vec(17.734, 96.859)), module, WTVCO::SYNC_PARAM)); | |||
| addParam(createLightParamCentered<LEDLightBezel<>>(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<LEDLightBezel<>>(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::WAVE_INPUT)); | |||
| // addInput(createInputCentered<PJ301MPort>(mm2px(Vec(6.897, 113.115)), module, WTVCO::PITCH_INPUT)); | |||
| addInput(createInputCentered<PJ301MPort>(mm2px(Vec(28.571, 96.859)), module, WTVCO::POS_INPUT)); | |||
| addInput(createInputCentered<PJ301MPort>(mm2px(Vec(6.897, 113.115)), module, WTVCO::PITCH_INPUT)); | |||
| addInput(createInputCentered<PJ301MPort>(mm2px(Vec(17.734, 113.115)), module, WTVCO::SYNC_INPUT)); | |||
| addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(28.571, 113.115)), module, WTVCO::OUT_OUTPUT)); | |||
| addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(28.571, 113.115)), module, WTVCO::WAVE_OUTPUT)); | |||
| addChild(createLightCentered<SmallLight<RedGreenBlueLight>>(mm2px(Vec(17.733, 49.409)), module, WTVCO::PHASE_LIGHT)); | |||
| addChild(createWidget<WTVCODisplay>(mm2px(Vec(0.0, 13.039)))); | |||
| WTDisplay<WTVCO>* display = createWidget<WTDisplay<WTVCO>>(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<WTVCO*>(this->module); | |||
| assert(module); | |||
| menu->addChild(new MenuSeparator); | |||
| module->wavetable.appendContextMenu(menu); | |||
| } | |||
| }; | |||
| @@ -0,0 +1,260 @@ | |||
| #pragma once | |||
| #include <rack.hpp> | |||
| #include <osdialog.h> | |||
| #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<float> 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<std::string> 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 <class TModule> | |||
| 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> 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); | |||
| } | |||
| }; | |||
| @@ -1,4 +1,4 @@ | |||
| #include "rack.hpp" | |||
| #include <rack.hpp> | |||
| using namespace rack; | |||