@@ -230,6 +230,17 @@ | |||
"Hardware clone", | |||
"Polyphonic" | |||
] | |||
}, | |||
{ | |||
"slug": "Edges", | |||
"name": "Quad Chiptune Audio Generator", | |||
"description": "Based on Mutable Instruments Edges", | |||
"manualUrl": "https://mutable-instruments.net/modules/edges/manual/", | |||
"modularGridUrl": "https://www.modulargrid.net/e/mutable-instruments-edges", | |||
"tags": [ | |||
"Oscillator", | |||
"Hardware clone" | |||
] | |||
} | |||
] | |||
} | |||
} |
@@ -0,0 +1,413 @@ | |||
#include "plugin.hpp" | |||
#include "Edges/digital_oscillator.hpp" | |||
#include "Edges/square_oscillator.hpp" | |||
using simd::float_4; | |||
// --------------------------------------------------------------------------- | |||
// MARK: Module | |||
// --------------------------------------------------------------------------- | |||
/// the PWM values to cycle between | |||
static const float SQUARE_PWM_VALUES[] = {0.5, 0.66, 0.75, 0.87, 0.95, 1.0}; | |||
struct Edges : Module { | |||
enum ParamIds { | |||
FREQ1_PARAM, | |||
XMOD1_PARAM, | |||
LEVEL1_PARAM, | |||
FREQ2_PARAM, | |||
XMOD2_PARAM, | |||
LEVEL2_PARAM, | |||
FREQ3_PARAM, | |||
XMOD3_PARAM, | |||
LEVEL3_PARAM, | |||
FREQ4_PARAM, | |||
LEVEL4_PARAM, | |||
WAVEFORM1_PARAM, | |||
WAVEFORM2_PARAM, | |||
WAVEFORM3_PARAM, | |||
WAVEFORM4_PARAM, | |||
NUM_PARAMS | |||
}; | |||
enum InputIds { | |||
GATE1_INPUT, | |||
FREQ1_INPUT, | |||
MOD1_INPUT, | |||
GATE2_INPUT, | |||
FREQ2_INPUT, | |||
MOD2_INPUT, | |||
GATE3_INPUT, | |||
FREQ3_INPUT, | |||
MOD3_INPUT, | |||
GATE4_INPUT, | |||
FREQ4_INPUT, | |||
MOD4_INPUT, | |||
NUM_INPUTS | |||
}; | |||
enum OutputIds { | |||
WAVEFORM1_OUTPUT, | |||
WAVEFORM2_OUTPUT, | |||
WAVEFORM3_OUTPUT, | |||
WAVEFORM4_OUTPUT, | |||
MIX_OUTPUT, | |||
NUM_OUTPUTS | |||
}; | |||
enum LightIds { | |||
GATE1_LIGHT_GREEN, GATE1_LIGHT_RED, | |||
GATE2_LIGHT_GREEN, GATE2_LIGHT_RED, | |||
GATE3_LIGHT_GREEN, GATE3_LIGHT_RED, | |||
GATE4_LIGHT_GREEN, GATE4_LIGHT_RED, | |||
NUM_LIGHTS | |||
}; | |||
Edges() { | |||
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | |||
// setup waveform 1 parameters | |||
configParam(FREQ1_PARAM, -36.0, 36.0, 0.0, "Channel 1 frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4); | |||
configParam(XMOD1_PARAM, 0.0, 1.0, 0.0, "Channel 1 to Channel 2 hard sync"); | |||
configParam(LEVEL1_PARAM, 0.0, 1.0, 0.0, "Channel 1 level", "%", 0, 100); | |||
// setup waveform 2 parameters | |||
configParam(FREQ2_PARAM, -36.0, 36.0, 0.0, "Channel 2 frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4); | |||
configParam(XMOD2_PARAM, 0.0, 1.0, 0.0, "Channel 1 x Channel 2 ring modulation"); | |||
configParam(LEVEL2_PARAM, 0.0, 1.0, 0.0, "Channel 2 level", "%", 0, 100); | |||
// setup waveform 3 parameters | |||
configParam(FREQ3_PARAM, -36.0, 36.0, 0.0, "Channel 3 frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4); | |||
configParam(XMOD3_PARAM, 0.0, 1.0, 0.0, "Channel 1 x Channel 3 ring modulation"); | |||
configParam(LEVEL3_PARAM, 0.0, 1.0, 0.0, "Channel 3 level", "%", 0, 100); | |||
// setup waveform 4 parameters | |||
configParam(FREQ4_PARAM, -36.0, 36.0, 0.0, "Channel 4 frequency", " Hz", dsp::FREQ_SEMITONE, dsp::FREQ_C4); | |||
configParam(LEVEL4_PARAM, 0.0, 1.0, 0.0, "Channel 4 level", "%", 0, 100); | |||
// setup waveform selection parameters | |||
configParam(WAVEFORM1_PARAM, 0.0, 1.0, 0.0, "Channel 1 select"); | |||
configParam(WAVEFORM2_PARAM, 0.0, 1.0, 0.0, "Channel 2 select"); | |||
configParam(WAVEFORM3_PARAM, 0.0, 1.0, 0.0, "Channel 3 select"); | |||
configParam(WAVEFORM4_PARAM, 0.0, 1.0, 0.0, "Channel 4 select"); | |||
} | |||
/// a type for the square wave oscillator | |||
typedef SquareWaveOscillator<16, 16, float_4> Square; | |||
/// a type for the digital oscillator | |||
typedef DigitalOscillator<float_4> Digital; | |||
/// channel 1 (square wave) | |||
Square oscillator1; | |||
/// channel 2 (square wave) | |||
Square oscillator2; | |||
/// channel 3 (square wave) | |||
Square oscillator3; | |||
/// channel 4 (digital selectable wave) | |||
Digital oscillator4; | |||
/// a flag determining whether waveform 1's gate is open | |||
bool is_gate1_open = true; | |||
/// a flag determining whether waveform 2's gate is open | |||
bool is_gate2_open = true; | |||
/// a flag determining whether waveform 3's gate is open | |||
bool is_gate3_open = true; | |||
/// a flag determining whether waveform 4's gate is open | |||
bool is_gate4_open = true; | |||
/// the voltage at 1V/oct port 1 | |||
float_4 voct1_voltage = 0.f; | |||
/// the voltage at 1V/oct port 2 | |||
float_4 voct2_voltage = 0.f; | |||
/// the voltage at 1V/oct port 3 | |||
float_4 voct3_voltage = 0.f; | |||
/// the voltage at 1V/oct port 4 | |||
float voct4_voltage = 0.f; | |||
/// the mod voltage for mod port 1 | |||
float_4 mod1_voltage = 0.f; | |||
/// the mod voltage for mod port 2 | |||
float_4 mod2_voltage = 0.f; | |||
/// the mod voltage for mod port 3 | |||
float_4 mod3_voltage = 0.f; | |||
/// the mod voltage for mod port 4 | |||
float mod4_voltage = 0.f; | |||
/// the state of oscillator 1 determined by the button | |||
int osc1_state = 0; | |||
/// the state of oscillator 2 determined by the button | |||
int osc2_state = 0; | |||
/// the state of oscillator 3 determined by the button | |||
int osc3_state = 0; | |||
/// a Schmitt trigger for handling inputs to the waveform select 1 button | |||
dsp::SchmittTrigger oscillator1Trigger; | |||
/// a Schmitt trigger for handling inputs to the waveform select 2 button | |||
dsp::SchmittTrigger oscillator2Trigger; | |||
/// a Schmitt trigger for handling inputs to the waveform select 3 button | |||
dsp::SchmittTrigger oscillator3Trigger; | |||
/// a Schmitt trigger for handling inputs to the waveform select 4 button | |||
dsp::SchmittTrigger oscillator4Trigger; | |||
/// Process the gate inputs to determine which waveforms are active. | |||
/// Note that Gates are normalled, meaning that when a connection is active | |||
/// at a higher level gate, the signal propagates to lower level gates that | |||
/// are not connected | |||
/// TODO: determine whether the 0.7V threshold from hardware specification | |||
/// fits the software model | |||
void process_gate_inputs() { | |||
// the 0.7V threshold is from the hardware specification for Edges | |||
is_gate1_open = inputs[GATE1_INPUT].getNormalVoltage(0.7f) >= 0.7f; | |||
is_gate2_open = inputs[GATE2_INPUT].getNormalVoltage(is_gate1_open * 0.7f) >= 0.7f; | |||
is_gate3_open = inputs[GATE3_INPUT].getNormalVoltage(is_gate2_open * 0.7f) >= 0.7f; | |||
is_gate4_open = inputs[GATE4_INPUT].getNormalVoltage(is_gate3_open * 0.7f) >= 0.7f; | |||
// set LEDs for each of the gates | |||
lights[GATE1_LIGHT_GREEN].setBrightness(is_gate1_open); | |||
lights[GATE2_LIGHT_GREEN].setBrightness(is_gate2_open); | |||
lights[GATE3_LIGHT_GREEN].setBrightness(is_gate3_open); | |||
lights[GATE4_LIGHT_GREEN].setBrightness(is_gate4_open); | |||
} | |||
/// Process the 1V/oct inputs | |||
void process_voct_inputs() { | |||
voct1_voltage = inputs[FREQ1_INPUT].getNormalVoltageSimd<float_4>(0, 0); | |||
voct2_voltage = inputs[FREQ2_INPUT].getNormalVoltageSimd<float_4>(voct1_voltage, 0); | |||
voct3_voltage = inputs[FREQ3_INPUT].getNormalVoltageSimd<float_4>(voct2_voltage, 0); | |||
// get the normalled channel 4 input as a float | |||
auto voct1_voltage_ = inputs[FREQ1_INPUT].getNormalVoltage(0, 0); | |||
auto voct2_voltage_ = inputs[FREQ2_INPUT].getNormalVoltage(voct1_voltage_, 0); | |||
auto voct3_voltage_ = inputs[FREQ3_INPUT].getNormalVoltage(voct2_voltage_, 0); | |||
voct4_voltage = inputs[FREQ4_INPUT].getNormalVoltage(voct3_voltage_, 0); | |||
// TODO: refactor to use the float_4 structure | |||
// voct4_voltage = inputs[FREQ4_INPUT].getNormalVoltageSimd<float_4>(voct3_voltage, 0); | |||
} | |||
inline void process_mod_inputs() { | |||
mod1_voltage = inputs[MOD1_INPUT].getNormalVoltageSimd<float_4>(0, 0); | |||
mod2_voltage = inputs[MOD2_INPUT].getNormalVoltageSimd<float_4>(0, 0); | |||
mod3_voltage = inputs[MOD3_INPUT].getNormalVoltageSimd<float_4>(0, 0); | |||
mod4_voltage = inputs[MOD4_INPUT].getNormalVoltage(0, 0); | |||
} | |||
void process_buttons() { | |||
// process a button press for oscillator 1 | |||
if (oscillator1Trigger.process(params[WAVEFORM1_PARAM].getValue())) { | |||
osc1_state = (osc1_state + 1) % 6; | |||
if (osc1_state < 5) { | |||
oscillator1.setPulseWidth(SQUARE_PWM_VALUES[osc1_state]); | |||
} | |||
} | |||
// set PWM for oscillator 1 based on oscillator 4 frequency control | |||
if (osc1_state == 5) { | |||
oscillator1.setPulseWidth(inputs[FREQ4_INPUT].getNormalVoltage(10.f) / 10.f); | |||
} | |||
// process a button press for oscillator 2 | |||
if (oscillator2Trigger.process(params[WAVEFORM2_PARAM].getValue())) { | |||
osc2_state = (osc2_state + 1) % 6; | |||
if (osc2_state < 5) { | |||
oscillator2.setPulseWidth(SQUARE_PWM_VALUES[osc2_state]); | |||
} | |||
} | |||
// set PWM for oscillator 2 based on oscillator 4 frequency control | |||
if (osc2_state == 5) { | |||
oscillator2.setPulseWidth(inputs[FREQ4_INPUT].getNormalVoltage(10.f) / 10.f); | |||
} | |||
// process a button press for oscillator 3 | |||
if (oscillator3Trigger.process(params[WAVEFORM3_PARAM].getValue())) { | |||
osc3_state = (osc3_state + 1) % 6; | |||
if (osc3_state < 5) { | |||
oscillator3.setPulseWidth(SQUARE_PWM_VALUES[osc3_state]); | |||
} | |||
} | |||
// set PWM for oscillator 3 based on oscillator 4 frequency control | |||
if (osc3_state == 5) { | |||
oscillator3.setPulseWidth(inputs[FREQ4_INPUT].getNormalVoltage(10.f) / 10.f); | |||
} | |||
// process a button press for oscillator 4 | |||
if (oscillator4Trigger.process(params[WAVEFORM4_PARAM].getValue())) { | |||
oscillator4.nextShape(); | |||
} | |||
} | |||
void process_xmod_switches() { | |||
oscillator2.syncEnabled = params[XMOD1_PARAM].getValue(); | |||
oscillator2.ringModulation = params[XMOD2_PARAM].getValue(); | |||
oscillator3.ringModulation = params[XMOD3_PARAM].getValue(); | |||
} | |||
void process_frequency(float sampleTime, int sampleRate) { | |||
// set the pitch of oscillator 1 | |||
float_4 pitch1 = params[FREQ1_PARAM].getValue() / 12.f; | |||
pitch1 += voct1_voltage; | |||
pitch1 += mod1_voltage; | |||
oscillator1.setPitch(pitch1); | |||
// set the pitch of oscillator 2 | |||
float_4 pitch2 = params[FREQ2_PARAM].getValue() / 12.f; | |||
pitch2 += voct2_voltage; | |||
pitch2 += mod2_voltage; | |||
oscillator2.setPitch(pitch2); | |||
// set the pitch of oscillator 3 | |||
float_4 pitch3 = params[FREQ3_PARAM].getValue() / 12.f; | |||
pitch3 += voct3_voltage; | |||
pitch3 += mod3_voltage; | |||
oscillator3.setPitch(pitch3); | |||
// set the pitch of oscillator 4 | |||
// get the pitch for the 4th oscillator (12 notes/octave * 1V/octave) | |||
auto twelve_volt_octave = 12 * (voct4_voltage + mod4_voltage); | |||
// 61 is the base for C4 | |||
uint16_t pitch4_int = 61 + params[FREQ4_PARAM].getValue() + twelve_volt_octave; | |||
// get the microtone for the 4th oscillator as 7-bit integer | |||
uint16_t pitch4_frac = 128 * (params[FREQ4_PARAM].getValue() - static_cast<int>(params[FREQ4_PARAM].getValue())); | |||
pitch4_frac += 128 * (twelve_volt_octave - static_cast<int>(twelve_volt_octave)); | |||
// set the pitch of oscillator 4 | |||
oscillator4.setPitch((pitch4_int << 7) + pitch4_frac + 64); | |||
// Process the output voltage of each oscillator | |||
oscillator1.process(sampleTime); | |||
// sync oscillator 2 to oscillator 1, ring mod with oscillator 1 | |||
// (hard sync is only applied if oscillator2.syncEnabled is true) | |||
// (ring mod is only applied if oscillator2.ringModulation is true) | |||
oscillator2.process(sampleTime, oscillator1.sqr(), oscillator1.sqr()); | |||
// don't sync oscillator 3, ring mod with oscillator 1 | |||
// (ring mod is only applied if oscillator3.ringModulation is true) | |||
oscillator3.process(sampleTime, 0, oscillator1.sqr()); | |||
oscillator4.process(sampleRate); | |||
} | |||
void process_output() { | |||
// set outputs for channel 1 if connected | |||
if (outputs[WAVEFORM1_OUTPUT].isConnected()) | |||
outputs[WAVEFORM1_OUTPUT].setVoltageSimd(is_gate1_open * 5.f * oscillator1.sqr(), 0); | |||
// set outputs for channel 2 if connected | |||
if (outputs[WAVEFORM2_OUTPUT].isConnected()) | |||
outputs[WAVEFORM2_OUTPUT].setVoltageSimd(is_gate2_open * 5.f * oscillator2.sqr(), 0); | |||
// set outputs for channel 3 if connected | |||
if (outputs[WAVEFORM3_OUTPUT].isConnected()) | |||
outputs[WAVEFORM3_OUTPUT].setVoltageSimd(is_gate3_open * 5.f * oscillator3.sqr(), 0); | |||
// set outputs for channel 4 if connected | |||
if (outputs[WAVEFORM4_OUTPUT].isConnected()) | |||
outputs[WAVEFORM4_OUTPUT].setVoltageSimd(is_gate4_open * 5.f * oscillator4.getValue(), 0); | |||
// create the mixed output if connected | |||
if (outputs[MIX_OUTPUT].isConnected()) { | |||
auto the_mix = is_gate1_open * oscillator1.sqr() * !outputs[WAVEFORM1_OUTPUT].isConnected() * params[LEVEL1_PARAM].getValue(); | |||
the_mix += is_gate2_open * oscillator2.sqr() * !outputs[WAVEFORM2_OUTPUT].isConnected() * params[LEVEL2_PARAM].getValue(); | |||
the_mix += is_gate3_open * oscillator3.sqr() * !outputs[WAVEFORM3_OUTPUT].isConnected() * params[LEVEL3_PARAM].getValue(); | |||
the_mix += is_gate4_open * oscillator4.getValue() * !outputs[WAVEFORM4_OUTPUT].isConnected() * params[LEVEL4_PARAM].getValue(); | |||
outputs[MIX_OUTPUT].setVoltageSimd(5.f * the_mix, 0); | |||
} | |||
} | |||
void process(const ProcessArgs &args) override { | |||
process_gate_inputs(); | |||
process_voct_inputs(); | |||
process_mod_inputs(); | |||
process_buttons(); | |||
process_xmod_switches(); | |||
process_frequency(args.sampleTime, args.sampleRate); | |||
process_output(); | |||
} | |||
}; | |||
// --------------------------------------------------------------------------- | |||
// MARK: Widget | |||
// --------------------------------------------------------------------------- | |||
/// A menu item for controlling the quantization feature | |||
template<typename T> | |||
struct EdgesQuantizerItem : MenuItem { | |||
/// the oscillator to control the quantization of | |||
T *oscillator; | |||
/// Respond to a menu action. | |||
inline void onAction(const event::Action &e) override { | |||
oscillator->isQuantized = not oscillator->isQuantized; | |||
} | |||
/// Perform a step on the menu. | |||
inline void step() override { | |||
rightText = oscillator->isQuantized ? "âś”" : ""; | |||
MenuItem::step(); | |||
} | |||
}; | |||
/// The widget structure that lays out the panel of the module and the UI menus. | |||
struct EdgesWidget : ModuleWidget { | |||
EdgesWidget(Edges *module) { | |||
setModule(module); | |||
setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Edges.svg"))); | |||
// add vanity screws | |||
addChild(createWidget<ScrewSilver>(Vec(15, 0))); | |||
addChild(createWidget<ScrewSilver>(Vec(270, 0))); | |||
addChild(createWidget<ScrewSilver>(Vec(15, 365))); | |||
addChild(createWidget<ScrewSilver>(Vec(270, 365))); | |||
// add frequency knobs | |||
addParam(createParam<Rogan1PSWhite>(Vec(122, 54), module, Edges::FREQ1_PARAM)); | |||
addParam(createParam<Rogan1PSWhite>(Vec(122, 114), module, Edges::FREQ2_PARAM)); | |||
addParam(createParam<Rogan1PSWhite>(Vec(122, 174), module, Edges::FREQ3_PARAM)); | |||
addParam(createParam<Rogan1PSWhite>(Vec(122, 234), module, Edges::FREQ4_PARAM)); | |||
// add XMOD switches | |||
addParam(createParam<CKSS>(Vec(178, 64), module, Edges::XMOD1_PARAM)); | |||
addParam(createParam<CKSS>(Vec(178, 124), module, Edges::XMOD2_PARAM)); | |||
addParam(createParam<CKSS>(Vec(178, 184), module, Edges::XMOD3_PARAM)); | |||
// add level knobs | |||
addParam(createParam<Rogan1PSGreen>(Vec(245, 54), module, Edges::LEVEL1_PARAM)); | |||
addParam(createParam<Rogan1PSGreen>(Vec(245, 114), module, Edges::LEVEL2_PARAM)); | |||
addParam(createParam<Rogan1PSGreen>(Vec(245, 174), module, Edges::LEVEL3_PARAM)); | |||
addParam(createParam<Rogan1PSGreen>(Vec(245, 234), module, Edges::LEVEL4_PARAM)); | |||
// add waveform selection buttons | |||
addParam(createParam<TL1105>(Vec(43, 320), module, Edges::WAVEFORM1_PARAM)); | |||
addParam(createParam<TL1105>(Vec(67, 320), module, Edges::WAVEFORM2_PARAM)); | |||
addParam(createParam<TL1105>(Vec(91, 320), module, Edges::WAVEFORM3_PARAM)); | |||
addParam(createParam<TL1105>(Vec(115, 320), module, Edges::WAVEFORM4_PARAM)); | |||
// add inputs for Gate | |||
addInput(createInput<PJ301MPort>(Vec(20, 62), module, Edges::GATE1_INPUT)); | |||
addInput(createInput<PJ301MPort>(Vec(20, 122), module, Edges::GATE2_INPUT)); | |||
addInput(createInput<PJ301MPort>(Vec(20, 182), module, Edges::GATE3_INPUT)); | |||
addInput(createInput<PJ301MPort>(Vec(20, 242), module, Edges::GATE4_INPUT)); | |||
// add inputs for Frequency (1V/Oct) | |||
addInput(createInput<PJ301MPort>(Vec(58, 62), module, Edges::FREQ1_INPUT)); | |||
addInput(createInput<PJ301MPort>(Vec(58, 122), module, Edges::FREQ2_INPUT)); | |||
addInput(createInput<PJ301MPort>(Vec(58, 182), module, Edges::FREQ3_INPUT)); | |||
addInput(createInput<PJ301MPort>(Vec(58, 242), module, Edges::FREQ4_INPUT)); | |||
// add inputs for Mod | |||
addInput(createInput<PJ301MPort>(Vec(90, 62), module, Edges::MOD1_INPUT)); | |||
addInput(createInput<PJ301MPort>(Vec(90, 122), module, Edges::MOD2_INPUT)); | |||
addInput(createInput<PJ301MPort>(Vec(90, 182), module, Edges::MOD3_INPUT)); | |||
addInput(createInput<PJ301MPort>(Vec(90, 242), module, Edges::MOD4_INPUT)); | |||
// add outputs for each waveform | |||
addOutput(createOutput<PJ301MPort>(Vec(215, 62), module, Edges::WAVEFORM1_OUTPUT)); | |||
addOutput(createOutput<PJ301MPort>(Vec(215, 122), module, Edges::WAVEFORM2_OUTPUT)); | |||
addOutput(createOutput<PJ301MPort>(Vec(215, 182), module, Edges::WAVEFORM3_OUTPUT)); | |||
addOutput(createOutput<PJ301MPort>(Vec(215, 242), module, Edges::WAVEFORM4_OUTPUT)); | |||
// add output for mix (all waveforms mixed) | |||
addOutput(createOutput<PJ301MPort>(Vec(253, 315), module, Edges::MIX_OUTPUT)); | |||
// add an LED for each waveform selection button | |||
addChild(createLight<MediumLight<GreenRedLight>>(Vec(46, 300), module, Edges::GATE1_LIGHT_GREEN)); | |||
addChild(createLight<MediumLight<GreenRedLight>>(Vec(70, 300), module, Edges::GATE2_LIGHT_GREEN)); | |||
addChild(createLight<MediumLight<GreenRedLight>>(Vec(94, 300), module, Edges::GATE3_LIGHT_GREEN)); | |||
addChild(createLight<MediumLight<GreenRedLight>>(Vec(118, 300), module, Edges::GATE4_LIGHT_GREEN)); | |||
} | |||
void appendContextMenu(Menu *menu) override { | |||
Edges *module = dynamic_cast<Edges*>(this->module); | |||
assert(module); | |||
// add the menu for the Quantizer feature | |||
menu->addChild(construct<MenuLabel>()); | |||
menu->addChild(construct<MenuLabel>(&MenuLabel::text, "Quantizer")); | |||
menu->addChild(construct<EdgesQuantizerItem<Edges::Square>>( | |||
&MenuItem::text, "Channel 1", | |||
&EdgesQuantizerItem<Edges::Square>::oscillator, &module->oscillator1 | |||
)); | |||
menu->addChild(construct<EdgesQuantizerItem<Edges::Square>>( | |||
&MenuItem::text, "Channel 2", | |||
&EdgesQuantizerItem<Edges::Square>::oscillator, &module->oscillator2 | |||
)); | |||
menu->addChild(construct<EdgesQuantizerItem<Edges::Square>>( | |||
&MenuItem::text, "Channel 3", | |||
&EdgesQuantizerItem<Edges::Square>::oscillator, &module->oscillator3 | |||
)); | |||
menu->addChild(construct<EdgesQuantizerItem<Edges::Digital>>( | |||
&MenuItem::text, "Channel 4", | |||
&EdgesQuantizerItem<Edges::Digital>::oscillator, &module->oscillator4 | |||
)); | |||
} | |||
}; | |||
Model *modelEdges = createModel<Edges, EdgesWidget>("Edges"); |
@@ -0,0 +1,378 @@ | |||
#include "wavetables.hpp" | |||
#include <algorithm> | |||
#ifndef DIGITAL_OSCILLATOR | |||
#define DIGITAL_OSCILLATOR | |||
// --------------------------------------------------------------------------- | |||
// MARK: 24-bit floating point | |||
// --------------------------------------------------------------------------- | |||
/// A 24-bit floating point number | |||
struct uint24_t { | |||
/// the integral piece of the number | |||
uint16_t integral; | |||
/// the fractional piece of the number | |||
uint8_t fractional; | |||
}; | |||
/// Return the sum of two uint24_t values. | |||
/// | |||
/// @param left the left operand | |||
/// @param right the right operand | |||
/// @returns the sum of the left and right operands | |||
/// | |||
static inline uint24_t operator+(uint24_t left, uint24_t right) { | |||
uint24_t result; | |||
uint32_t leftv = (static_cast<uint32_t>(left.integral) << 8) + left.fractional; | |||
uint32_t rightv = (static_cast<uint32_t>(right.integral) << 8) + right.fractional; | |||
uint32_t sum = leftv + rightv; | |||
result.integral = sum >> 8; | |||
result.fractional = sum & 0xff; | |||
return result; | |||
} | |||
/// Perform a right shift operation in place. | |||
/// | |||
/// @param value a reference to the uint24_t to shift right in place | |||
/// @returns a reference to value | |||
/// | |||
static inline uint24_t& operator>>=(uint24_t& value, uint8_t num_shifts) { | |||
while (num_shifts--) { | |||
uint32_t av = static_cast<uint32_t>(value.integral) << 8; | |||
av += value.fractional; | |||
av >>= 1; | |||
value.integral = av >> 8; | |||
value.fractional = av & 0xff; | |||
} | |||
return value; | |||
} | |||
// --------------------------------------------------------------------------- | |||
// MARK: Interpolation | |||
// --------------------------------------------------------------------------- | |||
/// Mix two 8-bit values. | |||
/// | |||
/// @param a the first value to mix | |||
/// @param b the second value to mix | |||
/// @param balance the mix between a (0) and b (255) | |||
/// | |||
static inline uint8_t mix(uint16_t a, uint16_t b, uint8_t balance) { | |||
return (a * (255 - balance) + b * balance) >> 8; | |||
} | |||
/// Interpolate between a sample from a wave table. | |||
/// | |||
/// @param table a pointer to the table to lookup samples in | |||
/// @param phase the current phase in the wave table | |||
/// | |||
static inline uint8_t interpolate(const uint8_t* table, uint16_t phase) { | |||
auto index = phase >> 7; | |||
return mix(table[index], table[index + 1], phase & 0xff); | |||
} | |||
/// Interpolate between two wave tables. | |||
/// | |||
/// @param table_a the first wave table | |||
/// @param table_b the second wave table | |||
/// @param phase the phase in the wave table | |||
/// @param gain_a the gain for the first wave | |||
/// @param gain_b the gain for the second wave | |||
/// @returns a value interpolated between the two wave tables at the given phase | |||
/// | |||
static inline uint16_t interpolate( | |||
const uint8_t* table_a, | |||
const uint8_t* table_b, | |||
uint16_t phase, | |||
uint8_t gain_a, | |||
uint8_t gain_b | |||
) { | |||
uint16_t result = 0; | |||
result += interpolate(table_a, phase) * gain_a; | |||
result += interpolate(table_b, phase) * gain_b; | |||
return result; | |||
} | |||
// --------------------------------------------------------------------------- | |||
// MARK: Digital Oscillator | |||
// --------------------------------------------------------------------------- | |||
/// The wave shapes for the digital oscillator | |||
enum OscillatorShape { | |||
OSC_SINE = 0, | |||
OSC_TRIANGLE, | |||
OSC_NES_TRIANGLE, | |||
OSC_PITCHED_NOISE, | |||
OSC_NES_NOISE_LONG, | |||
OSC_NES_NOISE_SHORT, | |||
NUM_DIGITAL_OSC // the total number of oscillators in the enumeration | |||
}; | |||
/// TODO: document | |||
static const uint8_t kMaxZone = 7; | |||
/// TODO: document | |||
static const int16_t kOctave = 12 * 128; | |||
/// TODO: document | |||
static const int16_t kPitchTableStart = 116 * 128; | |||
/// the native sample rate of the digital oscillator | |||
static const float SAMPLE_RATE = 48000.f; | |||
/// A digital oscillator with triangle, NES triangle, NES noise, hold & sample | |||
/// noise, and sine wave shapes. | |||
template<typename T> | |||
class DigitalOscillator { | |||
public: | |||
/// whether note quantization is enabled | |||
bool isQuantized = false; | |||
/// Initialize a new digital oscillator. | |||
DigitalOscillator() { intialize(); } | |||
/// Destroy an existing digital oscillator. | |||
~DigitalOscillator() { } | |||
/// Initialize the digital oscillator | |||
void intialize() { | |||
// set the pitch to the default value | |||
setPitch(60 << 7); | |||
// reset the wave shape to the first value | |||
shape_ = OSC_SINE; | |||
// set the gate to true (i.e., open) | |||
setGate(true); | |||
// reset the random number seed to 1 | |||
rng_state_ = 1; | |||
} | |||
/// Set the wave shape to a new value. | |||
/// | |||
/// @param shape_ the wave shape to select | |||
/// | |||
inline void setShape(OscillatorShape shape) { shape_ = shape; } | |||
/// Select the next wave shape. | |||
inline void nextShape() { | |||
auto next_shape = (static_cast<int>(shape_) + 1) % NUM_DIGITAL_OSC; | |||
shape_ = static_cast<OscillatorShape>(next_shape); | |||
} | |||
/// Set the pitch to a new value. | |||
/// | |||
/// @param pitch_ the pitch of the oscillator | |||
/// | |||
inline void setPitch(int16_t pitch) { | |||
if (isQuantized) pitch = pitch & 0b1111111110000000; | |||
pitch_ = pitch; | |||
} | |||
/// Set the gate to a new value. | |||
/// | |||
/// @param gate_ the gate of the oscillator | |||
/// | |||
inline void setGate(bool gate) { gate_ = gate; } | |||
/// Set the CV Pulse Width to a new value. | |||
/// | |||
/// @param cv_pw_ the CV parameter for the pulse width | |||
/// | |||
inline void set_cv_pw(uint8_t cv_pw) { cv_pw_ = cv_pw; } | |||
/// Render a sample from the oscillator using current parameters. | |||
void process(int sampleRate) { | |||
if (gate_) { | |||
computePhaseIncrement(sampleRate); | |||
(this->*getRenderFunction(shape_))(); | |||
} else { | |||
renderSilence(); | |||
} | |||
} | |||
/// Get the value from the oscillator in the range [-1.0, 1.0] | |||
inline T getValue() const { | |||
// divide the 12-bit value by 4096.0 to normalize in [0.0, 1.0] | |||
// multiply by 2 and subtract 1 to get the value in [-1.0, 1.0] | |||
return 2 * (value / 4096.0) - 1; | |||
} | |||
private: | |||
/// a type for calling and storing render functions for the wave shapes | |||
typedef void (DigitalOscillator::*RenderFunction)(); | |||
/// Get the render function for the given shape. | |||
/// | |||
/// @param shape the wave shape to return the render function for | |||
/// @returns the render function for the given wave shape | |||
/// | |||
static inline RenderFunction getRenderFunction(OscillatorShape shape) { | |||
// create a static constant array of the functions | |||
static const RenderFunction function_table[] = { | |||
&DigitalOscillator::renderSine, | |||
&DigitalOscillator::renderBandlimitedTriangle, | |||
&DigitalOscillator::renderBandlimitedTriangle, | |||
&DigitalOscillator::renderNoise, | |||
&DigitalOscillator::renderNoiseNES, | |||
&DigitalOscillator::renderNoiseNES | |||
}; | |||
// lookup the wave using the shape enumeration | |||
return function_table[shape]; | |||
} | |||
/// the current shape of the wave produced by the oscillator | |||
OscillatorShape shape_ = OSC_TRIANGLE; | |||
/// the current pitch of the oscillator | |||
int16_t pitch_ = 60 << 7; | |||
/// the 12TET quantized note based on the oscillator pitch | |||
uint8_t note_ = 0; | |||
/// whether the gate is open | |||
bool gate_ = true; | |||
/// the phase the oscillator is currently at | |||
uint24_t phase_; | |||
/// the change inn phase at the current processing step | |||
uint24_t phase_increment_; | |||
/// The random number generator state for generating random noise | |||
uint16_t rng_state_ = 1; | |||
/// A sample from the sine wave to use for the random noise generators | |||
uint16_t sample_ = 0; | |||
/// The auxiliary phase for the sine wave bit crusher | |||
uint16_t aux_phase_ = 0; | |||
/// The CV PW value for the sine wave bit crusher | |||
uint8_t cv_pw_ = 0; | |||
/// The output value from the oscillator | |||
uint16_t value = 0; | |||
/// Compute the increment in phased for the current step. | |||
/// | |||
/// @param sampleRate the sample rate to output audio at | |||
/// | |||
void computePhaseIncrement(int sampleRate) { | |||
int16_t ref_pitch = pitch_ - kPitchTableStart; | |||
uint8_t num_shifts = shape_ >= OSC_PITCHED_NOISE ? 0 : 1; | |||
while (ref_pitch < 0) { | |||
ref_pitch += kOctave; | |||
++num_shifts; | |||
} | |||
uint24_t increment; | |||
uint16_t pitch_lookup_index_integral = ref_pitch >> 4; | |||
uint8_t pitch_lookup_index_fractional = ref_pitch << 4; | |||
uint16_t increment16 = lut_res_oscillator_increments[pitch_lookup_index_integral]; | |||
uint16_t increment16_next = lut_res_oscillator_increments[pitch_lookup_index_integral + 1]; | |||
// set the integral and fractional of the increment | |||
uint32_t increment16_diff = increment16_next - increment16; | |||
uint32_t pitch32 = pitch_lookup_index_fractional; | |||
uint16_t integral_diff = (increment16_diff * pitch32) >> 8; | |||
// set the integral based o the current sample rate | |||
increment.integral = (SAMPLE_RATE / sampleRate) * increment16 + integral_diff; | |||
increment.fractional = 0; | |||
increment >>= num_shifts; | |||
// shift the 15-bit pitch over 7 bits to produce a byte, 12 is the min value | |||
note_ = std::max(pitch_ >> 7, 12); | |||
phase_increment_ = increment; | |||
} | |||
/// Run the sample loop with given callback for the loop body. | |||
/// | |||
/// @param render_fn a callback function that accepts the phase and phase | |||
/// increment as parameters | |||
/// | |||
template<typename Callable> | |||
inline void renderWrapper(Callable render_fn) { | |||
uint24_t phase; | |||
uint24_t phase_increment; | |||
phase_increment.integral = phase_increment_.integral; | |||
phase_increment.fractional = phase_increment_.fractional; | |||
phase.integral = phase_.integral; | |||
phase.fractional = phase_.fractional; | |||
render_fn(phase, phase_increment); | |||
phase_.integral = phase.integral; | |||
phase_.fractional = phase.fractional; | |||
} | |||
/// Render silence from the oscillator. | |||
inline void renderSilence() { value = 0; } | |||
/// Render a sine wave from the oscillator. | |||
void renderSine() { | |||
uint16_t aux_phase_increment = lut_res_bitcrusher_increments[cv_pw_]; | |||
renderWrapper([&, this](uint24_t& phase, uint24_t& phase_increment) { | |||
phase = phase + phase_increment; | |||
aux_phase_ += aux_phase_increment; | |||
if (aux_phase_ < aux_phase_increment || !aux_phase_increment) { | |||
sample_ = interpolate(wav_res_bandlimited_triangle_6, phase.integral) << 8; | |||
} | |||
value = sample_ >> 4; | |||
}); | |||
} | |||
/// Render a triangle wave from the oscillator. | |||
void renderBandlimitedTriangle() { | |||
uint8_t balance_index = ((note_ - 12) << 4) | ((note_ - 12) >> 4); | |||
uint8_t gain_2 = balance_index & 0xf0; | |||
uint8_t gain_1 = ~gain_2; | |||
uint8_t wave_index = balance_index & 0xf; | |||
uint8_t base_resource_id = (shape_ == OSC_NES_TRIANGLE) | |||
? WAV_RES_BANDLIMITED_NES_TRIANGLE_0 | |||
: WAV_RES_BANDLIMITED_TRIANGLE_0; | |||
const uint8_t* wave_1 = waveform_table[base_resource_id + wave_index]; | |||
wave_index = std::min<uint8_t>(wave_index + 1, kMaxZone); | |||
const uint8_t* wave_2 = waveform_table[base_resource_id + wave_index]; | |||
renderWrapper([&](uint24_t& phase, uint24_t& phase_increment) { | |||
phase = phase + phase_increment; | |||
uint16_t sample = interpolate(wave_1, wave_2, phase.integral, gain_1, gain_2); | |||
value = sample >> 4; | |||
}); | |||
} | |||
/// Render NES noise from the oscillator. | |||
void renderNoiseNES() { | |||
uint16_t rng_state = rng_state_; | |||
uint16_t sample = sample_; | |||
renderWrapper([&, this](uint24_t& phase, uint24_t& phase_increment) { | |||
phase = phase + phase_increment; | |||
if (phase.integral < phase_increment.integral) { | |||
uint8_t tap = rng_state >> 1; | |||
if (shape_ == OSC_NES_NOISE_SHORT) { | |||
tap >>= 5; | |||
} | |||
uint8_t random_bit = (rng_state ^ tap) & 1; | |||
rng_state >>= 1; | |||
if (random_bit) { | |||
rng_state |= 0x4000; | |||
sample = 0x0300; | |||
} else { | |||
sample = 0x0cff; | |||
} | |||
} | |||
value = sample; | |||
}); | |||
rng_state_ = rng_state; | |||
sample_ = sample; | |||
} | |||
/// Render sample and hold noise from the oscillator. | |||
void renderNoise() { | |||
uint16_t rng_state = rng_state_; | |||
uint16_t sample = sample_; | |||
renderWrapper([&](uint24_t& phase, uint24_t& phase_increment) { | |||
phase = phase + phase_increment; | |||
if (phase.integral < phase_increment.integral) { | |||
rng_state = (rng_state >> 1) ^ (-(rng_state & 1) & 0xb400); | |||
sample = rng_state & 0x0fff; | |||
sample = 512 + ((sample * 3) >> 2); | |||
} | |||
value = sample; | |||
}); | |||
rng_state_ = rng_state; | |||
sample_ = sample; | |||
} | |||
}; | |||
#endif // DIGITAL_OSCILLATOR |
@@ -0,0 +1,169 @@ | |||
#ifndef SQUARE_OSCILLATOR_HPP | |||
#define SQUARE_OSCILLATOR_HPP | |||
// --------------------------------------------------------------------------- | |||
// MARK: Square Oscillator | |||
// --------------------------------------------------------------------------- | |||
/// An oscillator that generates a square wave | |||
template <int OVERSAMPLE, int QUALITY, typename T> | |||
struct SquareWaveOscillator { | |||
/// whether the oscillator is emulating an analog oscillator | |||
bool analog = false; | |||
/// TODO: document | |||
bool soft = false; | |||
/// whether the oscillator is synced to another oscillator | |||
bool syncEnabled = false; | |||
/// Whether ring modulation is enabled | |||
bool ringModulation = false; | |||
/// whether note quantization is enabled | |||
bool isQuantized = false; | |||
// For optimizing in serial code | |||
int channels = 0; | |||
/// the value from the last oscillator synchronization | |||
T lastSyncValue = 0.f; | |||
/// the current phase in [0, 1] (2 * pi * phase) | |||
T phase = 0.f; | |||
/// the current frequency | |||
T freq; | |||
/// the current pulse width in [0, 1] | |||
T pulseWidth = 0.5f; | |||
/// the direction of the synchronization | |||
T syncDirection = 1.f; | |||
/// a filter for producing an analog effect on the square wave | |||
dsp::TRCFilter<T> sqrFilter; | |||
/// the minimum-phase band-limited step generator for preventing aliasing | |||
dsp::MinBlepGenerator<QUALITY, OVERSAMPLE, T> sqrMinBlep; | |||
/// The current value of the square wave | |||
T sqrValue = 0.f; | |||
/// Set the pitch of the oscillator to a new value. | |||
/// | |||
/// @param pitch the new pitch to set the oscillator to | |||
/// | |||
inline void setPitch(T pitch) { | |||
// quantized the pitch to semitone if enabled | |||
if (isQuantized) pitch = floor(pitch * 12) / 12.f; | |||
// set the frequency based on the pitch | |||
freq = dsp::FREQ_C4 * dsp::approxExp2_taylor5(pitch + 30) / 1073741824; | |||
} | |||
/// Set the pulse width of the square wave to a new value. | |||
/// | |||
/// @param pulseWidth the new pulse width to set the square wave to | |||
/// | |||
inline void setPulseWidth(T pulseWidth) { | |||
const float pwMin = 0.01f; | |||
this->pulseWidth = simd::clamp(pulseWidth, pwMin, 1.f - pwMin); | |||
} | |||
/// Process a sample for given change in time and sync value. | |||
/// | |||
/// @param deltaTime the change in time between samples | |||
/// @param syncValue the value of the oscillator to sync to | |||
/// @param modulator the value of the oscillator applying ring modulation | |||
/// | |||
void process(float deltaTime, T syncValue = 0, T modulator = 1) { | |||
// Advance phase | |||
T deltaPhase = simd::clamp(freq * deltaTime, 1e-6f, 0.35f); | |||
if (soft) // Reverse direction | |||
deltaPhase *= syncDirection; | |||
else // Reset back to forward | |||
syncDirection = 1.f; | |||
phase += deltaPhase; | |||
// Wrap phase | |||
phase -= simd::floor(phase); | |||
// Jump sqr when crossing 0, or 1 if backwards | |||
T wrapPhase = (syncDirection == -1.f) & 1.f; | |||
T wrapCrossing = (wrapPhase - (phase - deltaPhase)) / deltaPhase; | |||
int wrapMask = simd::movemask((0 < wrapCrossing) & (wrapCrossing <= 1.f)); | |||
if (wrapMask) { | |||
for (int i = 0; i < channels; i++) { | |||
if (wrapMask & (1 << i)) { | |||
T mask = simd::movemaskInverse<T>(1 << i); | |||
float p = wrapCrossing[i] - 1.f; | |||
T x = mask & (2.f * syncDirection); | |||
sqrMinBlep.insertDiscontinuity(p, x); | |||
} | |||
} | |||
} | |||
// Jump sqr when crossing `pulseWidth` | |||
T pulseCrossing = (pulseWidth - (phase - deltaPhase)) / deltaPhase; | |||
int pulseMask = simd::movemask((0 < pulseCrossing) & (pulseCrossing <= 1.f)); | |||
if (pulseMask) { | |||
for (int i = 0; i < channels; i++) { | |||
if (pulseMask & (1 << i)) { | |||
T mask = simd::movemaskInverse<T>(1 << i); | |||
float p = pulseCrossing[i] - 1.f; | |||
T x = mask & (-2.f * syncDirection); | |||
sqrMinBlep.insertDiscontinuity(p, x); | |||
} | |||
} | |||
} | |||
// Detect sync | |||
// Might be NAN or outside of [0, 1) range | |||
if (syncEnabled) { | |||
T deltaSync = syncValue - lastSyncValue; | |||
T syncCrossing = -lastSyncValue / deltaSync; | |||
lastSyncValue = syncValue; | |||
T sync = (0.f < syncCrossing) & (syncCrossing <= 1.f) & (syncValue >= 0.f); | |||
int syncMask = simd::movemask(sync); | |||
if (syncMask) { | |||
if (soft) { | |||
syncDirection = simd::ifelse(sync, -syncDirection, syncDirection); | |||
} | |||
else { | |||
T newPhase = simd::ifelse(sync, (1.f - syncCrossing) * deltaPhase, phase); | |||
// Insert minBLEP for sync | |||
for (int i = 0; i < channels; i++) { | |||
if (syncMask & (1 << i)) { | |||
T mask = simd::movemaskInverse<T>(1 << i); | |||
float p = syncCrossing[i] - 1.f; | |||
T x; | |||
x = mask & (sqr(newPhase) - sqr(phase)); | |||
sqrMinBlep.insertDiscontinuity(p, x); | |||
} | |||
} | |||
phase = newPhase; | |||
} | |||
} | |||
} | |||
// process the square wave value | |||
sqrValue = sqr(phase); | |||
sqrValue += sqrMinBlep.process(); | |||
if (analog) { // apply an analog filter | |||
sqrFilter.setCutoffFreq(20.f * deltaTime); | |||
sqrFilter.process(sqrValue); | |||
sqrValue = sqrFilter.highpass() * 0.95f; | |||
} | |||
if (ringModulation) { // apply ring modulation | |||
sqrValue *= modulator; | |||
} | |||
} | |||
/// Calculate and return the value of the square wave for given phase. | |||
/// | |||
/// @param phase the phase of the wave in [0, 1] | |||
/// @returns the value of the wave in [0, 1] | |||
/// | |||
inline T sqr(T phase) { return simd::ifelse(phase < pulseWidth, 1.f, -1.f); } | |||
/// Return the value of the square wave. | |||
/// | |||
/// @returns the value of the wave in [0, 1] | |||
/// | |||
inline T sqr() const { return sqrValue; } | |||
}; | |||
#endif // SQUARE_OSCILLATOR_HPP |
@@ -24,4 +24,5 @@ void init(rack::Plugin* p) { | |||
p->addModel(modelMarbles); | |||
p->addModel(modelStages); | |||
p->addModel(modelRipples); | |||
p->addModel(modelEdges); | |||
} |
@@ -24,3 +24,4 @@ extern Model* modelFrames; | |||
extern Model* modelStages; | |||
extern Model* modelMarbles; | |||
extern Model* modelRipples; | |||
extern Model *modelEdges; |