| @@ -24,8 +24,8 @@ | |||||
| }, | }, | ||||
| { | { | ||||
| "slug": "VCO2", | "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", | "manualUrl": "https://vcvrack.com/Fundamental#VCO", | ||||
| "tags": [ | "tags": [ | ||||
| "VCO", | "VCO", | ||||
| @@ -65,7 +65,7 @@ | |||||
| }, | }, | ||||
| { | { | ||||
| "slug": "LFO", | "slug": "LFO", | ||||
| "name": "LFO-1", | |||||
| "name": "LFO", | |||||
| "description": "Low-frequency oscillator", | "description": "Low-frequency oscillator", | ||||
| "manualUrl": "https://vcvrack.com/Fundamental#LFO", | "manualUrl": "https://vcvrack.com/Fundamental#LFO", | ||||
| "tags": [ | "tags": [ | ||||
| @@ -1,5 +1,5 @@ | |||||
| #include "plugin.hpp" | #include "plugin.hpp" | ||||
| #include "samplerate.h" | |||||
| #include <samplerate.h> | |||||
| #define HISTORY_SIZE (1<<21) | #define HISTORY_SIZE (1<<21) | ||||
| @@ -1,9 +1,5 @@ | |||||
| #include "plugin.hpp" | #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; | using simd::float_4; | ||||
| @@ -39,19 +35,14 @@ struct WTLFO : Module { | |||||
| NUM_LIGHTS | 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 offset = false; | ||||
| bool invert = false; | bool invert = false; | ||||
| std::string filename; | |||||
| float_4 phases[4] = {}; | float_4 phases[4] = {}; | ||||
| float lastPos = 0.f; | float lastPos = 0.f; | ||||
| float clockFreq = 1.f; | float clockFreq = 1.f; | ||||
| dsp::Timer clockTimer; | dsp::Timer clockTimer; | ||||
| bool clockEnabled = false; | |||||
| dsp::ClockDivider lightDivider; | dsp::ClockDivider lightDivider; | ||||
| dsp::BooleanTrigger offsetTrigger; | 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<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(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); | 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); | 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(FM_INPUT, "Frequency modulation"); | ||||
| configInput(RESET_INPUT, "Reset"); | configInput(RESET_INPUT, "Reset"); | ||||
| configInput(POS_INPUT, "Wavetable position"); | configInput(POS_INPUT, "Wavetable position"); | ||||
| configInput(CLOCK_INPUT, "Clock"); | configInput(CLOCK_INPUT, "Clock"); | ||||
| configOutput(WAVE_OUTPUT, "Wavetable"); | |||||
| configOutput(WAVE_OUTPUT, "Wave"); | |||||
| configLight(PHASE_LIGHT, "Phase"); | configLight(PHASE_LIGHT, "Phase"); | ||||
| lightDivider.setDivision(16); | lightDivider.setDivision(16); | ||||
| @@ -96,24 +89,9 @@ struct WTLFO : Module { | |||||
| void onReset(const ResetEvent& e) override { | void onReset(const ResetEvent& e) override { | ||||
| Module::onReset(e); | 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 | // Reset state | ||||
| for (int c = 0; c < 16; c += 4) { | 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() { | void clearOutput() { | ||||
| outputs[WAVE_OUTPUT].setVoltage(0.f); | outputs[WAVE_OUTPUT].setVoltage(0.f); | ||||
| outputs[WAVE_OUTPUT].setChannels(1); | outputs[WAVE_OUTPUT].setChannels(1); | ||||
| @@ -130,11 +127,6 @@ struct WTLFO : Module { | |||||
| } | } | ||||
| void process(const ProcessArgs& args) override { | 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)) | if (offsetTrigger.process(params[OFFSET_PARAM].getValue() > 0.f)) | ||||
| offset ^= true; | offset ^= true; | ||||
| if (invertTrigger.process(params[INVERT_PARAM].getValue() > 0.f)) | 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()); | 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 | // Clock | ||||
| if (inputs[CLOCK_INPUT].isConnected()) { | if (inputs[CLOCK_INPUT].isConnected()) { | ||||
| clockTimer.process(args.sampleTime); | clockTimer.process(args.sampleTime); | ||||
| @@ -170,6 +151,22 @@ struct WTLFO : Module { | |||||
| clockFreq = 2.f; | 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 | // Iterate channels | ||||
| for (int c = 0; c < channels; c += 4) { | for (int c = 0; c < channels; c += 4) { | ||||
| // Calculate frequency in Hz | // Calculate frequency in Hz | ||||
| @@ -187,35 +184,35 @@ struct WTLFO : Module { | |||||
| phase = simd::ifelse(reset, 0.f, phase); | phase = simd::ifelse(reset, 0.f, phase); | ||||
| phases[c / 4] = phase; | phases[c / 4] = phase; | ||||
| // Scale phase from 0 to waveLen | // 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; | float_4 pos = posParam + inputs[POS_INPUT].getPolyVoltageSimd<float_4>(c) * posCvParam / 10.f; | ||||
| pos = simd::clamp(pos); | pos = simd::clamp(pos); | ||||
| pos *= (wavetableLen - 1); | |||||
| pos *= (waveCount - 1); | |||||
| if (c == 0) | if (c == 0) | ||||
| lastPos = pos[0]; | lastPos = pos[0]; | ||||
| // Get wavetable points | // Get wavetable points | ||||
| float_4 out = 0.f; | 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 | // 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 | // 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; | size_t pos1 = pos0 + 1; | ||||
| // Get waves | // 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) { | 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 { | 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(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 "plugin.hpp" | ||||
| #include "Wavetable.hpp" | |||||
| #include <samplerate.h> | |||||
| using simd::float_4; | |||||
| struct WTVCO : Module { | struct WTVCO : Module { | ||||
| enum ParamIds { | enum ParamIds { | ||||
| MODE_PARAM, // removed | MODE_PARAM, // removed | ||||
| SYNC_PARAM, | |||||
| SOFT_PARAM, | |||||
| FREQ_PARAM, | FREQ_PARAM, | ||||
| WAVE_PARAM, | |||||
| POS_PARAM, | |||||
| FM_PARAM, | FM_PARAM, | ||||
| // added in 2.0 | |||||
| POS_CV_PARAM, | |||||
| LINEAR_PARAM, | |||||
| NUM_PARAMS | NUM_PARAMS | ||||
| }; | }; | ||||
| enum InputIds { | enum InputIds { | ||||
| FM_INPUT, | FM_INPUT, | ||||
| SYNC_INPUT, | SYNC_INPUT, | ||||
| WAVE_INPUT, | |||||
| POS_INPUT, | |||||
| // added in 2.0 | |||||
| PITCH_INPUT, | |||||
| NUM_INPUTS | NUM_INPUTS | ||||
| }; | }; | ||||
| enum OutputIds { | enum OutputIds { | ||||
| OUT_OUTPUT, | |||||
| WAVE_OUTPUT, | |||||
| NUM_OUTPUTS | NUM_OUTPUTS | ||||
| }; | }; | ||||
| enum LightIds { | enum LightIds { | ||||
| ENUMS(PHASE_LIGHT, 3), | ENUMS(PHASE_LIGHT, 3), | ||||
| SOFT_LIGHT, | |||||
| LINEAR_LIGHT, | |||||
| NUM_LIGHTS | 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::ClockDivider lightDivider; | ||||
| dsp::BooleanTrigger softTrigger; | |||||
| dsp::BooleanTrigger linearTrigger; | |||||
| WTVCO() { | WTVCO() { | ||||
| config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | 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(FM_INPUT, "Frequency modulation"); | ||||
| configInput(SYNC_INPUT, "Sync"); | 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); | 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 { | 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))); | 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(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<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(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)); | 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(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; | using namespace rack; | ||||