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