Browse Source

Implement most of WTVCO.

tags/v2.0.1
Andrew Belt 3 years ago
parent
commit
492d7b5bbb
6 changed files with 585 additions and 332 deletions
  1. +3
    -3
      plugin.json
  2. +1
    -1
      src/Delay.cpp
  3. +79
    -256
      src/WTLFO.cpp
  4. +241
    -71
      src/WTVCO.cpp
  5. +260
    -0
      src/Wavetable.hpp
  6. +1
    -1
      src/plugin.hpp

+ 3
- 3
plugin.json View File

@@ -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
- 1
src/Delay.cpp View File

@@ -1,5 +1,5 @@
#include "plugin.hpp"
#include "samplerate.h"
#include <samplerate.h>


#define HISTORY_SIZE (1<<21)


+ 79
- 256
src/WTLFO.cpp View File

@@ -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);
}
};



+ 241
- 71
src/WTVCO.cpp View File

@@ -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);
}
};



+ 260
- 0
src/Wavetable.hpp View File

@@ -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
- 1
src/plugin.hpp View File

@@ -1,4 +1,4 @@
#include "rack.hpp"
#include <rack.hpp>


using namespace rack;


Loading…
Cancel
Save