BogaudioModules

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

FMOp.cpp (12448B)


      1 
      2 #include "FMOp.hpp"
      3 #include "dsp/pitch.hpp"
      4 
      5 #define INTERPOLATION "interpolation"
      6 #define INTERPOLATION_VALUE_ON "on"
      7 #define INTERPOLATION_VALUE_OFF "off"
      8 #define LINEAR_LEVEL "linearLevel"
      9 #define ANTIALIAS_FEEDBACK "antialias_feedback"
     10 #define ANTIALIAS_DEPTH "antialias_depth"
     11 
     12 float FMOp::RatioParamQuantity::getDisplayValue() {
     13 	float v = getValue();
     14 	if (!module) {
     15 		return v;
     16 	}
     17 
     18 	if (v < 0.0f) {
     19 		return std::max(1.0f + v, 0.01f);
     20 	}
     21 	v *= 9.0f;
     22 	v += 1.0f;
     23 	return v;
     24 }
     25 
     26 void FMOp::RatioParamQuantity::setDisplayValue(float v) {
     27 	if (!module) {
     28 		return;
     29 	}
     30 
     31 	if (v < 1.0f) {
     32 		v = v - 1.0f;
     33 	}
     34 	else {
     35 		v -= 1.0f;
     36 		v /= 9.0f;
     37 	}
     38 	setValue(v);
     39 }
     40 
     41 bool FMOp::LevelParamQuantity::isLinear() {
     42 	return dynamic_cast<FMOp*>(module)->_linearLevel;
     43 }
     44 
     45 void FMOp::Engine::reset() {
     46 	envelope.reset();
     47 	gateTrigger.reset();
     48 }
     49 
     50 void FMOp::Engine::sampleRateChange() {
     51 	float sampleRate = APP->engine->getSampleRate();
     52 	envelope.setSampleRate(sampleRate);
     53 	phasor.setSampleRate(sampleRate);
     54 	decimator.setParams(sampleRate, oversample);
     55 	maxFrequency = 0.475f * sampleRate;
     56 	feedbackSL.setParams(sampleRate, 5.0f, 1.0f);
     57 	depthSL.setParams(sampleRate, 5.0f, 1.0f);
     58 	levelSL.setParams(sampleRate, 10.0f, 1.0f);
     59 	sustainSL.setParams(sampleRate, 1.0f, 1.0f);
     60 }
     61 
     62 json_t* FMOp::saveToJson(json_t* root) {
     63 	json_object_set_new(root, INTERPOLATION, json_string(_interpolation == SineTableOscillator::INTERPOLATION_ON ? INTERPOLATION_VALUE_ON : INTERPOLATION_VALUE_OFF));
     64 	json_object_set_new(root, LINEAR_LEVEL, json_boolean(_linearLevel));
     65 	json_object_set_new(root, ANTIALIAS_FEEDBACK, json_boolean(_antiAliasFeedback));
     66 	json_object_set_new(root, ANTIALIAS_DEPTH, json_boolean(_antiAliasDepth));
     67 	return root;
     68 }
     69 
     70 void FMOp::loadFromJson(json_t* root) {
     71 	json_t* i = json_object_get(root, INTERPOLATION);
     72 	if (i) {
     73 		const char *s = json_string_value(i);
     74 		if (strcmp(s, INTERPOLATION_VALUE_ON) == 0) {
     75 			_interpolation = SineTableOscillator::INTERPOLATION_ON;
     76 		}
     77 	}
     78 
     79 	json_t* ll = json_object_get(root, LINEAR_LEVEL);
     80 	if (ll) {
     81 		_linearLevel = json_is_true(ll);
     82 	}
     83 
     84 	json_t* aaf = json_object_get(root, ANTIALIAS_FEEDBACK);
     85 	if (aaf) {
     86 		_antiAliasFeedback = json_is_true(aaf);
     87 	}
     88 
     89 	json_t* aad = json_object_get(root, ANTIALIAS_DEPTH);
     90 	if (aad) {
     91 		_antiAliasDepth = json_is_true(aad);
     92 	}
     93 }
     94 
     95 void FMOp::reset() {
     96 	for (int c = 0; c < _channels; ++c) {
     97 		_engines[c]->reset();
     98 	}
     99 }
    100 
    101 void FMOp::sampleRateChange() {
    102 	for (int c = 0; c < _channels; ++c) {
    103 		_engines[c]->sampleRateChange();
    104 	}
    105 }
    106 
    107 bool FMOp::active() {
    108 	return outputs[AUDIO_OUTPUT].isConnected();
    109 }
    110 
    111 int FMOp::channels() {
    112 	return inputs[PITCH_INPUT].getChannels();
    113 }
    114 
    115 void FMOp::addChannel(int c) {
    116 	_engines[c] = new Engine();
    117 	_engines[c]->reset();
    118 	_engines[c]->sampleRateChange();
    119 	if (c > 0) {
    120 		_engines[c]->phasor.syncPhase(_engines[0]->phasor);
    121 	}
    122 }
    123 
    124 void FMOp::removeChannel(int c) {
    125 	delete _engines[c];
    126 	_engines[c] = NULL;
    127 }
    128 
    129 void FMOp::modulate() {
    130 	_levelEnvelopeOn = params[ENV_TO_LEVEL_PARAM].getValue() > 0.5f;
    131 	_feedbackEnvelopeOn = params[ENV_TO_FEEDBACK_PARAM].getValue() > 0.5f;
    132 	_depthEnvelopeOn = params[ENV_TO_DEPTH_PARAM].getValue() > 0.5f;
    133 }
    134 
    135 void FMOp::modulateChannel(int c) {
    136 	Engine& e = *_engines[c];
    137 
    138 	float pitchIn = 0.0f;
    139 	if (inputs[PITCH_INPUT].isConnected()) {
    140 		pitchIn = inputs[PITCH_INPUT].getVoltage(c);
    141 	}
    142 	float ratio = params[RATIO_PARAM].getValue();
    143 	if (ratio < 0.0f) {
    144 		ratio = std::max(1.0f + ratio, 0.01f);
    145 	}
    146 	else {
    147 		ratio *= 9.0f;
    148 		ratio += 1.0f;
    149 	}
    150 
    151 	float frequency = pitchIn;
    152 	frequency += params[FINE_PARAM].getValue() / 12.0f;
    153 	frequency = cvToFrequency(frequency);
    154 	frequency *= ratio;
    155 	frequency = clamp(frequency, -e.maxFrequency, e.maxFrequency);
    156 	e.phasor.setFrequency(frequency / (float)oversample);
    157 
    158 	bool envelopeOn = _levelEnvelopeOn || _feedbackEnvelopeOn || _depthEnvelopeOn;
    159 	if (envelopeOn && !e.envelopeOn) {
    160 		e.envelope.reset();
    161 	}
    162 	e.envelopeOn = envelopeOn;
    163 
    164 	if (e.envelopeOn) {
    165 		float sustain = params[SUSTAIN_PARAM].getValue();
    166 		if (inputs[SUSTAIN_INPUT].isConnected()) {
    167 			sustain *= clamp(inputs[SUSTAIN_INPUT].getPolyVoltage(c) / 10.0f, 0.0f, 1.0f);
    168 		}
    169 		e.envelope.setAttack(powf(params[ATTACK_PARAM].getValue(), 2.0f) * 10.f);
    170 		e.envelope.setDecay(powf(params[DECAY_PARAM].getValue(), 2.0f) * 10.f);
    171 		e.envelope.setSustain(e.sustainSL.next(sustain));
    172 		e.envelope.setRelease(powf(params[RELEASE_PARAM].getValue(), 2.0f) * 10.f);
    173 	}
    174 
    175 	e.feedback = params[FEEDBACK_PARAM].getValue();
    176 	if (inputs[FEEDBACK_INPUT].isConnected()) {
    177 		e.feedback *= clamp(inputs[FEEDBACK_INPUT].getPolyVoltage(c) / 10.0f, 0.0f, 1.0f);
    178 	}
    179 
    180 	e.depth = params[DEPTH_PARAM].getValue();
    181 	if (inputs[DEPTH_INPUT].isConnected()) {
    182 		e.depth *= clamp(inputs[DEPTH_INPUT].getPolyVoltage(c) / 10.0f, 0.0f, 1.0f);
    183 	}
    184 
    185 	e.level = params[LEVEL_PARAM].getValue();
    186 	if (inputs[LEVEL_INPUT].isConnected()) {
    187 		e.level *= clamp(inputs[LEVEL_INPUT].getPolyVoltage(c) / 10.0f, 0.0f, 1.0f);
    188 	}
    189 
    190 	e.sineTable.setInterpolation(_interpolation);
    191 }
    192 
    193 void FMOp::processAlways(const ProcessArgs& args) {
    194 	_attackLightSum = _decayLightSum = _sustainLightSum = _releaseLightSum = 0;
    195 }
    196 
    197 void FMOp::processChannel(const ProcessArgs& args, int c) {
    198 	Engine& e = *_engines[c];
    199 
    200 	float envelope = 0.0f;
    201 	if (e.envelopeOn) {
    202 		float gateIn = 0.0f;
    203 		if (inputs[GATE_INPUT].isConnected()) {
    204 			gateIn = inputs[GATE_INPUT].getPolyVoltage(c);
    205 		}
    206 		e.gateTrigger.process(gateIn);
    207 		e.envelope.setGate(e.gateTrigger.isHigh());
    208 		envelope = e.envelope.next();
    209 	}
    210 
    211 	float feedback = e.feedbackSL.next(e.feedback);
    212 	if (_feedbackEnvelopeOn) {
    213 		feedback *= envelope;
    214 	}
    215 	bool feedbackOn = feedback > 0.001f;
    216 
    217 	float out = e.levelSL.next(e.level);
    218 	if (_levelEnvelopeOn) {
    219 		out *= envelope;
    220 	}
    221 
    222 	float offset = 0.0f;
    223 	if (feedbackOn) {
    224 		offset = feedback * e.feedbackDelayedSample;
    225 	}
    226 
    227 	bool depthOn = false;
    228 	if (inputs[FM_INPUT].isConnected()) {
    229 		float depth = e.depthSL.next(e.depth);
    230 		if (_depthEnvelopeOn) {
    231 			depth *= envelope;
    232 		}
    233 		offset += inputs[FM_INPUT].getPolyVoltage(c) * depth * 2.0f;
    234 		depthOn = depth > 0.001f;
    235 	}
    236 
    237 	float sample = 0.0f;
    238 	if (out > 0.0001f) {
    239 		Phasor::phase_delta_t o = Phasor::radiansToPhase(offset);
    240 		if ((feedbackOn && _antiAliasFeedback) || (depthOn && _antiAliasDepth)) {
    241 			if (e.oversampleMix < 1.0f) {
    242 				e.oversampleMix += oversampleMixIncrement;
    243 			}
    244 		}
    245 		else if (e.oversampleMix > 0.0f) {
    246 			e.oversampleMix -= oversampleMixIncrement;
    247 		}
    248 
    249 		if (e.oversampleMix > 0.0f) {
    250 			for (int i = 0; i < oversample; ++i) {
    251 				e.phasor.advancePhase();
    252 				e.buffer[i] = e.sineTable.nextFromPhasor(e.phasor, o);
    253 			}
    254 			sample = e.oversampleMix * e.decimator.next(e.buffer);
    255 		}
    256 		else {
    257 			e.phasor.advancePhase(oversample);
    258 		}
    259 		if (e.oversampleMix < 1.0f) {
    260 			sample += (1.0f - e.oversampleMix) * e.sineTable.nextFromPhasor(e.phasor, o);
    261 		}
    262 
    263 		if (_linearLevel) {
    264 			sample *= out;
    265 		}
    266 		else {
    267 			out = (1.0f - out) * Amplifier::minDecibels;
    268 			e.amplifier.setLevel(out);
    269 			sample = e.amplifier.next(sample);
    270 		}
    271 	}
    272 	else {
    273 		e.phasor.advancePhase(oversample);
    274 	}
    275 
    276 	outputs[AUDIO_OUTPUT].setChannels(_channels);
    277 	outputs[AUDIO_OUTPUT].setVoltage(e.feedbackDelayedSample = amplitude * sample, c);
    278 
    279 	_attackLightSum += e.envelope.isStage(dsp::ADSR::ATTACK_STAGE);
    280 	_decayLightSum += e.envelope.isStage(dsp::ADSR::DECAY_STAGE);
    281 	_sustainLightSum += e.envelope.isStage(dsp::ADSR::SUSTAIN_STAGE);
    282 	_releaseLightSum += e.envelope.isStage(dsp::ADSR::RELEASE_STAGE);
    283 }
    284 
    285 void FMOp::postProcessAlways(const ProcessArgs& args) {
    286 	lights[ATTACK_LIGHT].value = _attackLightSum * _inverseChannels;
    287 	lights[DECAY_LIGHT].value = _decayLightSum * _inverseChannels;
    288 	lights[SUSTAIN_LIGHT].value = _sustainLightSum * _inverseChannels;
    289 	lights[RELEASE_LIGHT].value = _releaseLightSum * _inverseChannels;
    290 }
    291 
    292 struct FMOpWidget : BGModuleWidget {
    293 	static constexpr int hp = 10;
    294 
    295 	FMOpWidget(FMOp* module) {
    296 		setModule(module);
    297 		box.size = Vec(RACK_GRID_WIDTH * hp, RACK_GRID_HEIGHT);
    298 		setPanel(box.size, "FMOp");
    299 		createScrews();
    300 
    301 		// generated by svg_widgets.rb
    302 		auto ratioParamPosition = Vec(30.0, 45.0);
    303 		auto fineParamPosition = Vec(112.0, 57.0);
    304 		auto attackParamPosition = Vec(107.0, 94.0);
    305 		auto decayParamPosition = Vec(107.0, 139.0);
    306 		auto sustainParamPosition = Vec(107.0, 184.0);
    307 		auto releaseParamPosition = Vec(107.0, 229.0);
    308 		auto depthParamPosition = Vec(36.0, 106.0);
    309 		auto envToDepthParamPosition = Vec(54.5, 139.7);
    310 		auto feedbackParamPosition = Vec(36.0, 162.0);
    311 		auto envToFeedbackParamPosition = Vec(54.5, 195.7);
    312 		auto levelParamPosition = Vec(36.0, 218.0);
    313 		auto envToLevelParamPosition = Vec(54.5, 251.7);
    314 
    315 		auto depthInputPosition = Vec(15.0, 274.0);
    316 		auto feedbackInputPosition = Vec(47.0, 274.0);
    317 		auto levelInputPosition = Vec(79.0, 274.0);
    318 		auto sustainInputPosition = Vec(111.0, 274.0);
    319 		auto pitchInputPosition = Vec(15.0, 318.0);
    320 		auto fmInputPosition = Vec(47.0, 318.0);
    321 		auto gateInputPosition = Vec(79.0, 318.0);
    322 
    323 		auto audioOutputPosition = Vec(111.0, 318.0);
    324 
    325 		auto attackLightPosition = Vec(118.5, 123.0);
    326 		auto decayLightPosition = Vec(118.5, 168.0);
    327 		auto sustainLightPosition = Vec(118.5, 213.0);
    328 		auto releaseLightPosition = Vec(118.5, 258.0);
    329 		// end generated by svg_widgets.rb
    330 
    331 		addParam(createParam<Knob38>(ratioParamPosition, module, FMOp::RATIO_PARAM));
    332 		addParam(createParam<Knob16>(fineParamPosition, module, FMOp::FINE_PARAM));
    333 		addParam(createParam<Knob26>(attackParamPosition, module, FMOp::ATTACK_PARAM));
    334 		addParam(createParam<Knob26>(decayParamPosition, module, FMOp::DECAY_PARAM));
    335 		addParam(createParam<Knob26>(sustainParamPosition, module, FMOp::SUSTAIN_PARAM));
    336 		addParam(createParam<Knob26>(releaseParamPosition, module, FMOp::RELEASE_PARAM));
    337 		addParam(createParam<Knob26>(depthParamPosition, module, FMOp::DEPTH_PARAM));
    338 		addParam(createParam<Knob26>(feedbackParamPosition, module, FMOp::FEEDBACK_PARAM));
    339 		addParam(createParam<Knob26>(levelParamPosition, module, FMOp::LEVEL_PARAM));
    340 		addParam(createParam<IndicatorButtonGreen9>(envToLevelParamPosition, module, FMOp::ENV_TO_LEVEL_PARAM));
    341 		addParam(createParam<IndicatorButtonGreen9>(envToFeedbackParamPosition, module, FMOp::ENV_TO_FEEDBACK_PARAM));
    342 		addParam(createParam<IndicatorButtonGreen9>(envToDepthParamPosition, module, FMOp::ENV_TO_DEPTH_PARAM));
    343 
    344 		addInput(createInput<Port24>(sustainInputPosition, module, FMOp::SUSTAIN_INPUT));
    345 		addInput(createInput<Port24>(depthInputPosition, module, FMOp::DEPTH_INPUT));
    346 		addInput(createInput<Port24>(feedbackInputPosition, module, FMOp::FEEDBACK_INPUT));
    347 		addInput(createInput<Port24>(levelInputPosition, module, FMOp::LEVEL_INPUT));
    348 		addInput(createInput<Port24>(pitchInputPosition, module, FMOp::PITCH_INPUT));
    349 		addInput(createInput<Port24>(gateInputPosition, module, FMOp::GATE_INPUT));
    350 		addInput(createInput<Port24>(fmInputPosition, module, FMOp::FM_INPUT));
    351 
    352 		addOutput(createOutput<Port24>(audioOutputPosition, module, FMOp::AUDIO_OUTPUT));
    353 
    354 		addChild(createLight<BGTinyLight<GreenLight>>(attackLightPosition, module, FMOp::ATTACK_LIGHT));
    355 		addChild(createLight<BGTinyLight<GreenLight>>(decayLightPosition, module, FMOp::DECAY_LIGHT));
    356 		addChild(createLight<BGTinyLight<GreenLight>>(sustainLightPosition, module, FMOp::SUSTAIN_LIGHT));
    357 		addChild(createLight<BGTinyLight<GreenLight>>(releaseLightPosition, module, FMOp::RELEASE_LIGHT));
    358 	}
    359 
    360 	void contextMenu(Menu* menu) override {
    361 		auto fmop = dynamic_cast<FMOp*>(module);
    362 		assert(fmop);
    363 
    364 		OptionsMenuItem* om = new OptionsMenuItem("Oscillator mode");
    365 		om->addItem(OptionMenuItem("Classic (extra harmonics)", [fmop]() { return fmop->_interpolation == SineTableOscillator::INTERPOLATION_OFF; }, [fmop]() { fmop->_interpolation = SineTableOscillator::INTERPOLATION_OFF; }));
    366 		om->addItem(OptionMenuItem("Clean (pure sine)", [fmop]() { return fmop->_interpolation == SineTableOscillator::INTERPOLATION_ON; }, [fmop]() { fmop->_interpolation = SineTableOscillator::INTERPOLATION_ON; }));
    367 		OptionsMenuItem::addToMenu(om, menu);
    368 
    369 		menu->addChild(new BoolOptionMenuItem("Linear level response", [fmop]() { return &fmop->_linearLevel; }));
    370 
    371 		menu->addChild(new BoolOptionMenuItem("Anti-alias feedback", [fmop]() { return &fmop->_antiAliasFeedback; }));
    372 		menu->addChild(new BoolOptionMenuItem("Anti-alias external FM", [fmop]() { return &fmop->_antiAliasDepth; }));
    373 	}
    374 };
    375 
    376 Model* modelFMOp = bogaudio::createModel<FMOp, FMOpWidget>("Bogaudio-FMOp", "FM-OP", "FM operator / oscillator / synth voice", "Oscillator", "Synth voice", "Polyphonic");