@@ -230,6 +230,17 @@ | |||||
"Hardware clone", | "Hardware clone", | ||||
"Polyphonic" | "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(modelMarbles); | ||||
p->addModel(modelStages); | p->addModel(modelStages); | ||||
p->addModel(modelRipples); | p->addModel(modelRipples); | ||||
p->addModel(modelEdges); | |||||
} | } |
@@ -24,3 +24,4 @@ extern Model* modelFrames; | |||||
extern Model* modelStages; | extern Model* modelStages; | ||||
extern Model* modelMarbles; | extern Model* modelMarbles; | ||||
extern Model* modelRipples; | extern Model* modelRipples; | ||||
extern Model *modelEdges; |