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