BogaudioModules

BogaudioModules for VCV Rack
Log | Files | Refs | README | LICENSE

XCO.cpp (17772B)


      1 
      2 #include "XCO.hpp"
      3 #include "dsp/pitch.hpp"
      4 
      5 #define DC_CORRECTION "dc_correction"
      6 #define CLIPPING_MODE "clipping_mode"
      7 
      8 float XCO::XCOFrequencyParamQuantity::offset() {
      9 	auto xco = dynamic_cast<XCO*>(module);
     10 	return xco->_slowMode ? xco->_slowModeOffset : 0.0f;
     11 }
     12 
     13 void XCO::Engine::reset() {
     14 	syncTrigger.reset();
     15 }
     16 
     17 void XCO::Engine::sampleRateChange(float sampleRate) {
     18 	phasor.setSampleRate(sampleRate);
     19 	square.setSampleRate(sampleRate);
     20 	saw.setSampleRate(sampleRate);
     21 
     22 	squareDecimator.setParams(sampleRate, oversample);
     23 	sawDecimator.setParams(sampleRate, oversample);
     24 	triangleDecimator.setParams(sampleRate, oversample);
     25 	sineDecimator.setParams(sampleRate, oversample);
     26 
     27 	fmDepthSL.setParams(sampleRate, 5.0f, 1.0f);
     28 	squarePulseWidthSL.setParams(sampleRate, 0.1f, 2.0f);
     29 	sawSaturationSL.setParams(sampleRate, 1.0f, 1.0f);
     30 	triangleSampleWidthSL.setParams(sampleRate, 0.1f, 1.0f);
     31 	sineFeedbackSL.setParams(sampleRate, 0.1f, 1.0f);
     32 	squareMixSL.setParams(sampleRate, 5.0f, 1.0f);
     33 	sawMixSL.setParams(sampleRate, 5.0f, 1.0f);
     34 	triangleMixSL.setParams(sampleRate, 5.0f, 1.0f);
     35 	sineMixSL.setParams(sampleRate, 5.0f, 1.0f);
     36 }
     37 
     38 void XCO::Engine::setFrequency(float f) {
     39 	if (frequency != f && frequency < 0.475f * phasor._sampleRate) {
     40 		frequency = f;
     41 		phasor.setFrequency(frequency / (float)oversample);
     42 		square.setFrequency(frequency);
     43 		saw.setFrequency(frequency);
     44 	}
     45 }
     46 
     47 void XCO::reset() {
     48 	for (int c = 0; c < _channels; ++c) {
     49 		_engines[c]->reset();
     50 	}
     51 }
     52 
     53 void XCO::sampleRateChange() {
     54 	float sampleRate = APP->engine->getSampleRate();
     55 	_oversampleThreshold = 0.06f * sampleRate;
     56 
     57 	for (int c = 0; c < _channels; ++c) {
     58 		_engines[c]->sampleRateChange(sampleRate);
     59 	}
     60 }
     61 
     62 json_t* XCO::saveToJson(json_t* root) {
     63 	json_object_set_new(root, DC_CORRECTION, json_boolean(_dcCorrection));
     64 	json_object_set_new(root, CLIPPING_MODE, json_integer(_clippingMode));
     65 	return root;
     66 }
     67 
     68 void XCO::loadFromJson(json_t* root) {
     69 	json_t* dc = json_object_get(root, DC_CORRECTION);
     70 	if (dc) {
     71 		_dcCorrection = json_boolean_value(dc);
     72 	}
     73 
     74 	json_t* c = json_object_get(root, CLIPPING_MODE);
     75 	if (c) {
     76 		_clippingMode = (Clipping)json_integer_value(c);
     77 		if (_clippingMode != SOFT_CLIPPING && _clippingMode != HARD_CLIPPING && _clippingMode != NO_CLIPPING) {
     78 			_clippingMode = COMP_CLIPPING;
     79 		}
     80 	}
     81 }
     82 
     83 bool XCO::active() {
     84 	return (
     85 		outputs[MIX_OUTPUT].isConnected() ||
     86 		outputs[SQUARE_OUTPUT].isConnected() ||
     87 		outputs[SAW_OUTPUT].isConnected() ||
     88 		outputs[TRIANGLE_OUTPUT].isConnected() ||
     89 		outputs[SINE_OUTPUT].isConnected()
     90 	);
     91 }
     92 
     93 int XCO::channels() {
     94 	return inputs[PITCH_INPUT].getChannels();
     95 }
     96 
     97 void XCO::addChannel(int c) {
     98 	_engines[c] = new Engine();
     99 	_engines[c]->reset();
    100 	_engines[c]->sampleRateChange(APP->engine->getSampleRate());
    101 	if (c > 0) {
    102 		_engines[c]->phasor.syncPhase(_engines[0]->phasor);
    103 	}
    104 }
    105 
    106 void XCO::removeChannel(int c) {
    107 	delete _engines[c];
    108 	_engines[c] = NULL;
    109 }
    110 
    111 void XCO::modulate() {
    112 	_slowMode = params[SLOW_PARAM].getValue() > 0.5f;
    113 	_fmLinearMode = params[FM_TYPE_PARAM].getValue() < 0.5f;
    114 }
    115 
    116 void XCO::modulateChannel(int c) {
    117 	Engine& e = *_engines[c];
    118 
    119 	e.baseVOct = params[FREQUENCY_PARAM].getValue();
    120 	e.baseVOct += params[FINE_PARAM].getValue() / 12.0f;;
    121 	if (inputs[PITCH_INPUT].isConnected()) {
    122 		e.baseVOct += clamp(inputs[PITCH_INPUT].getVoltage(c), -5.0f, 5.0f);
    123 	}
    124 	if (_slowMode) {
    125 		e.baseVOct += _slowModeOffset;
    126 	}
    127 	e.baseHz = cvToFrequency(e.baseVOct);
    128 
    129 	float pw = params[SQUARE_PW_PARAM].getValue();
    130 	if (inputs[SQUARE_PW_INPUT].isConnected()) {
    131 		pw *= clamp(inputs[SQUARE_PW_INPUT].getPolyVoltage(c) / 5.0f, -1.0f, 1.0f);
    132 	}
    133 	pw *= 1.0f - 2.0f * e.square.minPulseWidth;
    134 	pw *= 0.5f;
    135 	pw += 0.5f;
    136 	e.square.setPulseWidth(e.squarePulseWidthSL.next(pw), _dcCorrection);
    137 
    138 	float saturation = params[SAW_SATURATION_PARAM].getValue();
    139 	if (inputs[SAW_SATURATION_INPUT].isConnected()) {
    140 		saturation *= clamp(inputs[SAW_SATURATION_INPUT].getPolyVoltage(c) / 10.0f, 0.0f, 1.0f);
    141 	}
    142 	e.saw.setSaturation(e.sawSaturationSL.next(saturation) * 10.f);
    143 
    144 	float tsw = params[TRIANGLE_SAMPLE_PARAM].getValue() * Phasor::maxSampleWidth;
    145 	if (inputs[TRIANGLE_SAMPLE_INPUT].isConnected()) {
    146 		tsw *= clamp(inputs[TRIANGLE_SAMPLE_INPUT].getPolyVoltage(c) / 10.0f, 0.0f, 1.0f);
    147 	}
    148 	e.triangleSampleWidth = e.triangleSampleWidthSL.next(tsw);
    149 	e.triangle.setSampleWidth(e.triangleSampleWidth);
    150 
    151 	float sfb = params[SINE_FEEDBACK_PARAM].getValue();
    152 	if (inputs[SINE_FEEDBACK_INPUT].isConnected()) {
    153 		sfb *= clamp(inputs[SINE_FEEDBACK_INPUT].getPolyVoltage(c) / 10.0f, 0.0f, 1.0f);
    154 	}
    155 	e.sineFeedback = e.sineFeedbackSL.next(sfb);
    156 
    157 	e.fmDepth = params[FM_DEPTH_PARAM].getValue();
    158 	if (inputs[FM_DEPTH_INPUT].isConnected()) {
    159 		e.fmDepth *= clamp(inputs[FM_DEPTH_INPUT].getPolyVoltage(c) / 10.0f, 0.0f, 1.0f);
    160 	}
    161 
    162 	e.squarePhaseOffset = phaseOffset(c, params[SQUARE_PHASE_PARAM], inputs[SQUARE_PHASE_INPUT]);
    163 	e.sawPhaseOffset = phaseOffset(c, params[SAW_PHASE_PARAM], inputs[SAW_PHASE_INPUT]);
    164 	e.trianglePhaseOffset = phaseOffset(c, params[TRIANGLE_PHASE_PARAM], inputs[TRIANGLE_PHASE_INPUT]);
    165 	e.sinePhaseOffset = phaseOffset(c, params[SINE_PHASE_PARAM], inputs[SINE_PHASE_INPUT]);
    166 
    167 	e.squareMix = level(c, params[SQUARE_MIX_PARAM], inputs[SQUARE_MIX_INPUT]);
    168 	e.sawMix = level(c, params[SAW_MIX_PARAM], inputs[SAW_MIX_INPUT]);
    169 	e.triangleMix = level(c, params[TRIANGLE_MIX_PARAM], inputs[TRIANGLE_MIX_INPUT]);
    170 	e.sineMix = level(c, params[SINE_MIX_PARAM], inputs[SINE_MIX_INPUT]);
    171 }
    172 
    173 void XCO::processChannel(const ProcessArgs& args, int c) {
    174 	Engine& e = *_engines[c];
    175 
    176 	if (e.syncTrigger.next(inputs[SYNC_INPUT].getPolyVoltage(c))) {
    177 		e.phasor.resetPhase();
    178 	}
    179 
    180 	float frequency = e.baseHz;
    181 	Phasor::phase_delta_t phaseOffset = 0;
    182 	float fmd = e.fmDepthSL.next(e.fmDepth);
    183 	if (inputs[FM_INPUT].isConnected() && fmd > 0.01f) {
    184 		float fm = inputs[FM_INPUT].getPolyVoltage(c) * fmd;
    185 		if (_fmLinearMode) {
    186 			phaseOffset = Phasor::radiansToPhase(2.0f * fm);
    187 		}
    188 		else {
    189 			frequency = cvToFrequency(e.baseVOct + fm);
    190 		}
    191 	}
    192 	e.setFrequency(frequency);
    193 
    194 	const float oversampleWidth = 100.0f;
    195 	float mix, oMix;
    196 	if (frequency > _oversampleThreshold) {
    197 		if (frequency > _oversampleThreshold + oversampleWidth) {
    198 			mix = 0.0f;
    199 			oMix = 1.0f;
    200 		}
    201 		else {
    202 			oMix = (frequency - _oversampleThreshold) / oversampleWidth;
    203 			mix = 1.0f - oMix;
    204 		}
    205 	}
    206 	else {
    207 		mix = 1.0f;
    208 		oMix = 0.0f;
    209 	}
    210 
    211 	bool triangleSample = e.triangleSampleWidth > 0.001f;
    212 	bool squareActive = outputs[MIX_OUTPUT].isConnected() || outputs[SQUARE_OUTPUT].isConnected();
    213 	bool sawActive = outputs[MIX_OUTPUT].isConnected() || outputs[SAW_OUTPUT].isConnected();
    214 	bool triangleActive = outputs[MIX_OUTPUT].isConnected() || outputs[TRIANGLE_OUTPUT].isConnected();
    215 	bool sineActive = outputs[MIX_OUTPUT].isConnected() || outputs[SINE_OUTPUT].isConnected();
    216 	bool squareOversample = squareActive && oMix > 0.0f;
    217 	bool sawOversample = sawActive && oMix > 0.0f;
    218 	bool triangleOversample = triangleActive && (triangleSample || oMix > 0.0f);
    219 	bool squareNormal = squareActive && mix > 0.0f;
    220 	bool sawNormal = sawActive && mix > 0.0f;
    221 	bool triangleNormal = triangleActive && !triangleSample && mix > 0.0f;
    222 	float squareOut = 0.0f;
    223 	float sawOut = 0.0f;
    224 	float triangleOut = 0.0f;
    225 	float sineOut = 0.0f;
    226 
    227 	Phasor::phase_delta_t sineFeedbackOffset = 0;
    228 	if (sineActive) {
    229 		 if (e.sineFeedback > 0.001f) {
    230 			 sineFeedbackOffset = Phasor::radiansToPhase(e.sineFeedback * e.sineFeedbackDelayedSample);
    231 			 if (e.sineOMix < 1.0f) {
    232 				 e.sineOMix += sineOversampleMixIncrement;
    233 			 }
    234 		 }
    235 		 else if (e.sineOMix > 0.0f) {
    236 			 e.sineOMix -= sineOversampleMixIncrement;
    237 		 }
    238 	}
    239 
    240 	if (squareOversample || sawOversample || triangleOversample || e.sineOMix > 0.0f) {
    241 		for (int i = 0; i < Engine::oversample; ++i) {
    242 			e.phasor.advancePhase();
    243 			if (squareOversample) {
    244 				e.squareBuffer[i] = e.square.nextFromPhasor(e.phasor, e.squarePhaseOffset + phaseOffset);
    245 			}
    246 			if (sawOversample) {
    247 				e.sawBuffer[i] = e.saw.nextFromPhasor(e.phasor, e.sawPhaseOffset + phaseOffset);
    248 			}
    249 			if (triangleOversample) {
    250 				e.triangleBuffer[i] = e.triangle.nextFromPhasor(e.phasor, e.trianglePhaseOffset + phaseOffset);
    251 			}
    252 			if (e.sineOMix > 0.0f) {
    253 				e.sineBuffer[i] = e.sine.nextFromPhasor(e.phasor, sineFeedbackOffset + e.sinePhaseOffset + phaseOffset);
    254 			}
    255 		}
    256 		if (squareOversample) {
    257 			squareOut += oMix * amplitude * e.squareDecimator.next(e.squareBuffer);
    258 		}
    259 		if (sawOversample) {
    260 			sawOut += oMix * amplitude * e.sawDecimator.next(e.sawBuffer);
    261 		}
    262 		if (triangleOversample) {
    263 			triangleOut += amplitude * e.triangleDecimator.next(e.triangleBuffer);
    264 			if (!triangleSample) {
    265 				triangleOut *= oMix;
    266 			}
    267 		}
    268 		if (e.sineOMix > 0.0f) {
    269 			sineOut += amplitude * e.sineOMix * e.sineDecimator.next(e.sineBuffer);
    270 		}
    271 	}
    272 	else {
    273 		e.phasor.advancePhase(Engine::oversample);
    274 	}
    275 
    276 	if (squareNormal) {
    277 		squareOut += mix * amplitude * e.square.nextFromPhasor(e.phasor, e.squarePhaseOffset + phaseOffset);
    278 	}
    279 	if (sawNormal) {
    280 		sawOut += mix * amplitude * e.saw.nextFromPhasor(e.phasor, e.sawPhaseOffset + phaseOffset);
    281 	}
    282 	if (triangleNormal) {
    283 		triangleOut += mix * amplitude * e.triangle.nextFromPhasor(e.phasor, e.trianglePhaseOffset + phaseOffset);
    284 	}
    285 	if (e.sineOMix < 1.0f) {
    286 		sineOut += amplitude * (1.0f - e.sineOMix) * e.sine.nextFromPhasor(e.phasor, sineFeedbackOffset + e.sinePhaseOffset + phaseOffset);
    287 	}
    288 
    289 	outputs[SQUARE_OUTPUT].setChannels(_channels);
    290 	outputs[SQUARE_OUTPUT].setVoltage(squareOut, c);
    291 	outputs[SAW_OUTPUT].setChannels(_channels);
    292 	outputs[SAW_OUTPUT].setVoltage(sawOut, c);
    293 	outputs[TRIANGLE_OUTPUT].setChannels(_channels);
    294 	outputs[TRIANGLE_OUTPUT].setVoltage(triangleOut, c);
    295 	outputs[SINE_OUTPUT].setChannels(_channels);
    296 	outputs[SINE_OUTPUT].setVoltage(e.sineFeedbackDelayedSample = sineOut, c);
    297 	if (outputs[MIX_OUTPUT].isConnected()) {
    298 		float mix = e.squareMixSL.next(e.squareMix) * squareOut;
    299 		mix += e.sawMixSL.next(e.sawMix) * sawOut;
    300 		mix += e.triangleMixSL.next(e.triangleMix) * triangleOut;
    301 		mix += e.sineMixSL.next(e.sineMix) * sineOut;
    302 
    303 		switch (_clippingMode) {
    304 			case COMP_CLIPPING: {
    305 				Phasor::phase_t cycle = e.phasor._phase / Phasor::cyclePhase;
    306 				if (e.lastCycle != cycle) {
    307 					e.lastCycle = cycle;
    308 					e.mixScale = 1.0f / ((-e.minMix + e.maxMix) / 10.0f);
    309 					e.minMix = 0.0f;
    310 					e.maxMix = 0.0f;
    311 				} else if (mix < e.minMix) {
    312 					e.minMix = mix;
    313 				} else if (mix > e.maxMix) {
    314 					e.maxMix = mix;
    315 				}
    316 				if (e.mixScale < 1.0f) {
    317 					mix *= e.mixScale;
    318 				}
    319 				mix = clamp(mix, -12.0f, 12.0f);
    320 				break;
    321 			}
    322 			case SOFT_CLIPPING: {
    323 				mix = e.saturator.next(mix);
    324 				break;
    325 			}
    326 			case HARD_CLIPPING: {
    327 				mix = clamp(mix, -12.0f, 12.0f);
    328 				break;
    329 			}
    330 			case NO_CLIPPING:;
    331 		}
    332 
    333 		outputs[MIX_OUTPUT].setChannels(_channels);
    334 		outputs[MIX_OUTPUT].setVoltage(mix, c);
    335 	}
    336 }
    337 
    338 Phasor::phase_delta_t XCO::phaseOffset(int c, Param& param, Input& input) {
    339 	float v = param.getValue();
    340 	if (input.isConnected()) {
    341 		v *= clamp(input.getPolyVoltage(c) / 5.0f, -1.0f, 1.0f);
    342 	}
    343 	return -v * Phasor::cyclePhase / 2.0f;
    344 }
    345 
    346 float XCO::level(int c, Param& param, Input& input) {
    347 	float v = param.getValue();
    348 	if (input.isConnected()) {
    349 		v *= clamp(input.getPolyVoltage(c) / 10.0f, 0.0f, 1.0f);
    350 	}
    351 	return v;
    352 }
    353 
    354 struct XCOWidget : BGModuleWidget {
    355 	static constexpr int hp = 20;
    356 
    357 	XCOWidget(XCO* module) {
    358 		setModule(module);
    359 		box.size = Vec(RACK_GRID_WIDTH * hp, RACK_GRID_HEIGHT);
    360 		setPanel(box.size, "XCO");
    361 		createScrews();
    362 
    363 		// generated by svg_widgets.rb
    364 		auto frequencyParamPosition = Vec(40.0, 45.0);
    365 		auto fineParamPosition = Vec(47.0, 153.0);
    366 		auto slowParamPosition = Vec(112.0, 157.2);
    367 		auto fmDepthParamPosition = Vec(55.0, 194.0);
    368 		auto fmTypeParamPosition = Vec(101.5, 256.5);
    369 		auto squarePwParamPosition = Vec(147.0, 60.0);
    370 		auto squarePhaseParamPosition = Vec(147.0, 148.0);
    371 		auto squareMixParamPosition = Vec(147.0, 237.0);
    372 		auto sawSaturationParamPosition = Vec(187.0, 60.0);
    373 		auto sawPhaseParamPosition = Vec(187.0, 148.0);
    374 		auto sawMixParamPosition = Vec(187.0, 237.0);
    375 		auto triangleSampleParamPosition = Vec(227.0, 60.0);
    376 		auto trianglePhaseParamPosition = Vec(227.0, 148.0);
    377 		auto triangleMixParamPosition = Vec(227.0, 237.0);
    378 		auto sineFeedbackParamPosition = Vec(267.0, 60.0);
    379 		auto sinePhaseParamPosition = Vec(267.0, 148.0);
    380 		auto sineMixParamPosition = Vec(267.0, 237.0);
    381 
    382 		auto fmInputPosition = Vec(29.0, 251.0);
    383 		auto fmDepthInputPosition = Vec(62.0, 251.0);
    384 		auto squarePwInputPosition = Vec(143.0, 95.0);
    385 		auto squarePhaseInputPosition = Vec(143.0, 183.0);
    386 		auto squareMixInputPosition = Vec(143.0, 272.0);
    387 		auto sawSaturationInputPosition = Vec(183.0, 95.0);
    388 		auto sawPhaseInputPosition = Vec(183.0, 183.0);
    389 		auto sawMixInputPosition = Vec(183.0, 272.0);
    390 		auto triangleSampleInputPosition = Vec(223.0, 95.0);
    391 		auto trianglePhaseInputPosition = Vec(223.0, 183.0);
    392 		auto triangleMixInputPosition = Vec(223.0, 272.0);
    393 		auto sineFeedbackInputPosition = Vec(263.0, 95.0);
    394 		auto sinePhaseInputPosition = Vec(263.0, 183.0);
    395 		auto sineMixInputPosition = Vec(263.0, 272.0);
    396 		auto pitchInputPosition = Vec(17.0, 318.0);
    397 		auto syncInputPosition = Vec(50.0, 318.0);
    398 
    399 		auto squareOutputPosition = Vec(143.0, 318.0);
    400 		auto sawOutputPosition = Vec(183.0, 318.0);
    401 		auto triangleOutputPosition = Vec(223.0, 318.0);
    402 		auto sineOutputPosition = Vec(263.0, 318.0);
    403 		auto mixOutputPosition = Vec(103.0, 318.0);
    404 		// end generated by svg_widgets.rb
    405 
    406 		addParam(createParam<Knob68>(frequencyParamPosition, module, XCO::FREQUENCY_PARAM));
    407 		addParam(createParam<Knob16>(fineParamPosition, module, XCO::FINE_PARAM));
    408 		addParam(createParam<IndicatorButtonGreen9>(slowParamPosition, module, XCO::SLOW_PARAM));
    409 		addParam(createParam<Knob38>(fmDepthParamPosition, module, XCO::FM_DEPTH_PARAM));
    410 		addParam(createParam<SliderSwitch2State14>(fmTypeParamPosition, module, XCO::FM_TYPE_PARAM));
    411 		addParam(createParam<Knob16>(squarePwParamPosition, module, XCO::SQUARE_PW_PARAM));
    412 		addParam(createParam<Knob16>(squarePhaseParamPosition, module, XCO::SQUARE_PHASE_PARAM));
    413 		addParam(createParam<Knob16>(squareMixParamPosition, module, XCO::SQUARE_MIX_PARAM));
    414 		addParam(createParam<Knob16>(sawSaturationParamPosition, module, XCO::SAW_SATURATION_PARAM));
    415 		addParam(createParam<Knob16>(sawPhaseParamPosition, module, XCO::SAW_PHASE_PARAM));
    416 		addParam(createParam<Knob16>(sawMixParamPosition, module, XCO::SAW_MIX_PARAM));
    417 		addParam(createParam<Knob16>(triangleSampleParamPosition, module, XCO::TRIANGLE_SAMPLE_PARAM));
    418 		addParam(createParam<Knob16>(trianglePhaseParamPosition, module, XCO::TRIANGLE_PHASE_PARAM));
    419 		addParam(createParam<Knob16>(triangleMixParamPosition, module, XCO::TRIANGLE_MIX_PARAM));
    420 		addParam(createParam<Knob16>(sineFeedbackParamPosition, module, XCO::SINE_FEEDBACK_PARAM));
    421 		addParam(createParam<Knob16>(sinePhaseParamPosition, module, XCO::SINE_PHASE_PARAM));
    422 		addParam(createParam<Knob16>(sineMixParamPosition, module, XCO::SINE_MIX_PARAM));
    423 
    424 		addInput(createInput<Port24>(fmInputPosition, module, XCO::FM_INPUT));
    425 		addInput(createInput<Port24>(fmDepthInputPosition, module, XCO::FM_DEPTH_INPUT));
    426 		addInput(createInput<Port24>(squarePwInputPosition, module, XCO::SQUARE_PW_INPUT));
    427 		addInput(createInput<Port24>(squarePhaseInputPosition, module, XCO::SQUARE_PHASE_INPUT));
    428 		addInput(createInput<Port24>(squareMixInputPosition, module, XCO::SQUARE_MIX_INPUT));
    429 		addInput(createInput<Port24>(sawSaturationInputPosition, module, XCO::SAW_SATURATION_INPUT));
    430 		addInput(createInput<Port24>(sawPhaseInputPosition, module, XCO::SAW_PHASE_INPUT));
    431 		addInput(createInput<Port24>(sawMixInputPosition, module, XCO::SAW_MIX_INPUT));
    432 		addInput(createInput<Port24>(triangleSampleInputPosition, module, XCO::TRIANGLE_SAMPLE_INPUT));
    433 		addInput(createInput<Port24>(trianglePhaseInputPosition, module, XCO::TRIANGLE_PHASE_INPUT));
    434 		addInput(createInput<Port24>(triangleMixInputPosition, module, XCO::TRIANGLE_MIX_INPUT));
    435 		addInput(createInput<Port24>(sineFeedbackInputPosition, module, XCO::SINE_FEEDBACK_INPUT));
    436 		addInput(createInput<Port24>(sinePhaseInputPosition, module, XCO::SINE_PHASE_INPUT));
    437 		addInput(createInput<Port24>(sineMixInputPosition, module, XCO::SINE_MIX_INPUT));
    438 		addInput(createInput<Port24>(pitchInputPosition, module, XCO::PITCH_INPUT));
    439 		addInput(createInput<Port24>(syncInputPosition, module, XCO::SYNC_INPUT));
    440 
    441 		addOutput(createOutput<Port24>(squareOutputPosition, module, XCO::SQUARE_OUTPUT));
    442 		addOutput(createOutput<Port24>(sawOutputPosition, module, XCO::SAW_OUTPUT));
    443 		addOutput(createOutput<Port24>(triangleOutputPosition, module, XCO::TRIANGLE_OUTPUT));
    444 		addOutput(createOutput<Port24>(sineOutputPosition, module, XCO::SINE_OUTPUT));
    445 		addOutput(createOutput<Port24>(mixOutputPosition, module, XCO::MIX_OUTPUT));
    446 	}
    447 
    448 	void contextMenu(Menu* menu) override {
    449 		auto m = dynamic_cast<XCO*>(module);
    450 		assert(m);
    451 
    452 		menu->addChild(new BoolOptionMenuItem("DC offset correction", [m]() { return &m->_dcCorrection; }));
    453 
    454 		OptionsMenuItem* c = new OptionsMenuItem("Mix output processing");
    455 		c->addItem(OptionMenuItem("Scaled to +/-5V", [m]() { return m->_clippingMode == XCO::COMP_CLIPPING; }, [m]() { m->_clippingMode = XCO::COMP_CLIPPING; }));
    456 		c->addItem(OptionMenuItem("Saturated", [m]() { return m->_clippingMode == XCO::SOFT_CLIPPING; }, [m]() { m->_clippingMode = XCO::SOFT_CLIPPING; }));
    457 		c->addItem(OptionMenuItem("Hard clipped", [m]() { return m->_clippingMode == XCO::HARD_CLIPPING; }, [m]() { m->_clippingMode = XCO::HARD_CLIPPING; }));
    458 		c->addItem(OptionMenuItem("None", [m]() { return m->_clippingMode == XCO::NO_CLIPPING; }, [m]() { m->_clippingMode = XCO::NO_CLIPPING; }));
    459 		OptionsMenuItem::addToMenu(c, menu);
    460 	}
    461 };
    462 
    463 Model* modelXCO = bogaudio::createModel<XCO, XCOWidget>("Bogaudio-XCO", "XCO", "Oscillator", "Oscillator", "Polyphonic");