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