|
@@ -37,8 +37,6 @@ struct WTVCO : Module { |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
Wavetable wavetable; |
|
|
Wavetable wavetable; |
|
|
bool soft = false; |
|
|
|
|
|
bool linear = false; |
|
|
|
|
|
|
|
|
|
|
|
float_4 phases[4] = {}; |
|
|
float_4 phases[4] = {}; |
|
|
float lastPos = 0.f; |
|
|
float lastPos = 0.f; |
|
@@ -49,39 +47,37 @@ struct WTVCO : Module { |
|
|
|
|
|
|
|
|
WTVCO() { |
|
|
WTVCO() { |
|
|
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); |
|
|
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); |
|
|
configButton(SOFT_PARAM, "Soft sync"); |
|
|
|
|
|
configButton(LINEAR_PARAM, "Linear frequency modulation"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
configSwitch(SOFT_PARAM, 0.f, 1.f, 0.f, "Sync", {"Hard", "Soft"}); |
|
|
|
|
|
configSwitch(LINEAR_PARAM, 0.f, 1.f, 0.f, "Linear FM"); |
|
|
|
|
|
|
|
|
configParam(FREQ_PARAM, -75.f, 75.f, 0.f, "Frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4); |
|
|
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(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; |
|
|
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; |
|
|
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(POS_INPUT, "Wavetable position"); |
|
|
configInput(POS_INPUT, "Wavetable position"); |
|
|
configInput(PITCH_INPUT, "1V/octave pitch"); |
|
|
configInput(PITCH_INPUT, "1V/octave pitch"); |
|
|
|
|
|
|
|
|
configOutput(WAVE_OUTPUT, "Wave"); |
|
|
configOutput(WAVE_OUTPUT, "Wave"); |
|
|
|
|
|
|
|
|
configLight(PHASE_LIGHT, "Phase"); |
|
|
configLight(PHASE_LIGHT, "Phase"); |
|
|
|
|
|
|
|
|
lightDivider.setDivision(16); |
|
|
|
|
|
wavetable.octaves = 8; |
|
|
|
|
|
wavetable.setQuality(4); |
|
|
wavetable.setQuality(4); |
|
|
|
|
|
|
|
|
|
|
|
lightDivider.setDivision(16); |
|
|
|
|
|
|
|
|
onReset(); |
|
|
onReset(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
void onReset() override { |
|
|
void onReset() override { |
|
|
soft = false; |
|
|
|
|
|
linear = false; |
|
|
|
|
|
wavetable.reset(); |
|
|
wavetable.reset(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
void onRandomize() override { |
|
|
|
|
|
soft = random::get<bool>(); |
|
|
|
|
|
linear = random::get<bool>(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void onAdd(const AddEvent& e) override { |
|
|
void onAdd(const AddEvent& e) override { |
|
|
std::string path = system::join(getPatchStorageDirectory(), "wavetable.wav"); |
|
|
std::string path = system::join(getPatchStorageDirectory(), "wavetable.wav"); |
|
|
// Silently fails |
|
|
// Silently fails |
|
@@ -104,80 +100,76 @@ struct WTVCO : Module { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
void process(const ProcessArgs& args) override { |
|
|
void process(const ProcessArgs& args) override { |
|
|
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 freqParam = params[FREQ_PARAM].getValue(); |
|
|
float fmParam = params[FM_PARAM].getValue(); |
|
|
float fmParam = params[FM_PARAM].getValue(); |
|
|
float posParam = params[POS_PARAM].getValue(); |
|
|
float posParam = params[POS_PARAM].getValue(); |
|
|
float posCvParam = params[POS_CV_PARAM].getValue(); |
|
|
float posCvParam = params[POS_CV_PARAM].getValue(); |
|
|
|
|
|
bool soft = params[SOFT_PARAM].getValue() > 0.f; |
|
|
|
|
|
bool linear = params[LINEAR_PARAM].getValue() > 0.f; |
|
|
|
|
|
|
|
|
// Check valid wave and wavetable size |
|
|
|
|
|
if (wavetable.waveLen < 2) { |
|
|
|
|
|
clearOutput(); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
int waveCount = wavetable.getWaveCount(); |
|
|
|
|
|
if (waveCount < 1) { |
|
|
|
|
|
clearOutput(); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
int channels = std::max({1, inputs[PITCH_INPUT].getChannels(), inputs[FM_INPUT].getChannels()}); |
|
|
|
|
|
|
|
|
// 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); |
|
|
|
|
|
|
|
|
|
|
|
// Number of octaves above frequency until Nyquist |
|
|
|
|
|
float_4 octave = simd::log2(args.sampleRate / 2 / freq); |
|
|
|
|
|
|
|
|
|
|
|
// Accumulate phase |
|
|
|
|
|
float_4 phase = phases[c / 4]; |
|
|
|
|
|
phase += freq * args.sampleTime; |
|
|
|
|
|
// Wrap phase |
|
|
|
|
|
phase -= simd::trunc(phase); |
|
|
|
|
|
phases[c / 4] = phase; |
|
|
|
|
|
// Scale phase from 0 to waveLen |
|
|
|
|
|
phase *= (wavetable.waveLen * wavetable.quality); |
|
|
|
|
|
|
|
|
|
|
|
// 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++) { |
|
|
|
|
|
// Get wave indexes |
|
|
|
|
|
float phaseF = phase[cc] - std::trunc(phase[cc]); |
|
|
|
|
|
size_t i0 = std::trunc(phase[cc]); |
|
|
|
|
|
size_t i1 = (i0 + 1) % (wavetable.waveLen * wavetable.quality); |
|
|
|
|
|
// Get pos indexes |
|
|
|
|
|
float posF = pos[cc] - std::trunc(pos[cc]); |
|
|
|
|
|
size_t pos0 = std::trunc(pos[cc]); |
|
|
|
|
|
size_t pos1 = pos0 + 1; |
|
|
|
|
|
// Get waves |
|
|
|
|
|
int octave0 = clamp((int) octave[cc], 0, 7); |
|
|
|
|
|
float out0 = crossfade(wavetable.interpolatedAt(octave0, pos0, i0), wavetable.interpolatedAt(octave0, pos0, i1), phaseF); |
|
|
|
|
|
if (posF > 0.f) { |
|
|
|
|
|
float out1 = crossfade(wavetable.interpolatedAt(octave0, pos1, i0), wavetable.interpolatedAt(octave0, pos1, i1), phaseF); |
|
|
|
|
|
out[cc] = crossfade(out0, out1, posF); |
|
|
|
|
|
} |
|
|
|
|
|
else { |
|
|
|
|
|
out[cc] = out0; |
|
|
|
|
|
|
|
|
int waveCount = wavetable.getWaveCount(); |
|
|
|
|
|
if (wavetable.waveLen >= 2 && waveCount >= 1) { |
|
|
|
|
|
// 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 * dsp::approxExp2_taylor5(pitch + 30.f) / std::pow(2.f, 30.f); |
|
|
|
|
|
|
|
|
|
|
|
// Limit to Nyquist frequency |
|
|
|
|
|
freq = simd::fmin(freq, args.sampleRate / 2); |
|
|
|
|
|
float_4 nyquistRatio = args.sampleRate / 2 / freq; |
|
|
|
|
|
|
|
|
|
|
|
// Accumulate phase |
|
|
|
|
|
float_4 phase = phases[c / 4]; |
|
|
|
|
|
phase += freq * args.sampleTime; |
|
|
|
|
|
// Wrap phase |
|
|
|
|
|
phase -= simd::trunc(phase); |
|
|
|
|
|
phases[c / 4] = phase; |
|
|
|
|
|
// Scale phase from 0 to waveLen |
|
|
|
|
|
phase *= (wavetable.waveLen * wavetable.quality); |
|
|
|
|
|
|
|
|
|
|
|
// 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++) { |
|
|
|
|
|
// Get wave indexes |
|
|
|
|
|
float phaseF = phase[cc] - std::trunc(phase[cc]); |
|
|
|
|
|
size_t i0 = std::trunc(phase[cc]); |
|
|
|
|
|
size_t i1 = (i0 + 1) % (wavetable.waveLen * wavetable.quality); |
|
|
|
|
|
// Get pos indexes |
|
|
|
|
|
float posF = pos[cc] - std::trunc(pos[cc]); |
|
|
|
|
|
size_t pos0 = std::trunc(pos[cc]); |
|
|
|
|
|
size_t pos1 = pos0 + 1; |
|
|
|
|
|
// Get waves |
|
|
|
|
|
// TODO Interpolate octaves |
|
|
|
|
|
int octave0 = math::log2((int) nyquistRatio[cc]); |
|
|
|
|
|
octave0 = clamp(octave0, 0, (int) wavetable.octaves - 1); |
|
|
|
|
|
float out0 = crossfade(wavetable.interpolatedAt(octave0, pos0, i0), wavetable.interpolatedAt(octave0, pos0, i1), phaseF); |
|
|
|
|
|
if (posF > 0.f) { |
|
|
|
|
|
float out1 = crossfade(wavetable.interpolatedAt(octave0, pos1, i0), wavetable.interpolatedAt(octave0, pos1, i1), phaseF); |
|
|
|
|
|
out[cc] = crossfade(out0, out1, posF); |
|
|
|
|
|
} |
|
|
|
|
|
else { |
|
|
|
|
|
out[cc] = out0; |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
outputs[WAVE_OUTPUT].setVoltageSimd(out * 5.f, c); |
|
|
|
|
|
|
|
|
outputs[WAVE_OUTPUT].setVoltageSimd(out * 5.f, c); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
else { |
|
|
|
|
|
// Wavetable is invalid, so set 0V |
|
|
|
|
|
for (int c = 0; c < channels; c += 4) { |
|
|
|
|
|
outputs[WAVE_OUTPUT].setVoltageSimd(float_4(0.f), c); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
outputs[WAVE_OUTPUT].setChannels(channels); |
|
|
outputs[WAVE_OUTPUT].setChannels(channels); |
|
@@ -202,10 +194,6 @@ struct WTVCO : Module { |
|
|
|
|
|
|
|
|
json_t* dataToJson() override { |
|
|
json_t* dataToJson() override { |
|
|
json_t* rootJ = json_object(); |
|
|
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 |
|
|
// Merge wavetable |
|
|
json_t* wavetableJ = wavetable.toJson(); |
|
|
json_t* wavetableJ = wavetable.toJson(); |
|
|
json_object_update(rootJ, wavetableJ); |
|
|
json_object_update(rootJ, wavetableJ); |
|
@@ -214,14 +202,6 @@ struct WTVCO : Module { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
void dataFromJson(json_t* rootJ) override { |
|
|
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 |
|
|
wavetable.fromJson(rootJ); |
|
|
wavetable.fromJson(rootJ); |
|
|
} |
|
|
} |
|
@@ -241,9 +221,9 @@ struct WTVCOWidget : ModuleWidget { |
|
|
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::POS_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(createLightParamCentered<LEDLightButton<MediumSimpleLight<YellowLight>>>(mm2px(Vec(17.734, 80.603)), module, WTVCO::LINEAR_PARAM, WTVCO::LINEAR_LIGHT)); |
|
|
|
|
|
|
|
|
addParam(createLightParamCentered<LEDLightLatch<MediumSimpleLight<WhiteLight>>>(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(createParamCentered<Trimpot>(mm2px(Vec(28.571, 80.603)), module, WTVCO::POS_CV_PARAM)); |
|
|
addParam(createLightParamCentered<LEDLightButton<MediumSimpleLight<YellowLight>>>(mm2px(Vec(17.734, 96.859)), module, WTVCO::SOFT_PARAM, WTVCO::SOFT_LIGHT)); |
|
|
|
|
|
|
|
|
addParam(createLightParamCentered<LEDLightLatch<MediumSimpleLight<WhiteLight>>>(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::POS_INPUT)); |
|
|
addInput(createInputCentered<PJ301MPort>(mm2px(Vec(28.571, 96.859)), module, WTVCO::POS_INPUT)); |
|
|