#include "FMOp.hpp" #include "dsp/pitch.hpp" #define LINEAR_LEVEL "linearLevel" void FMOp::onReset() { _steps = modulationSteps; _envelope.reset(); _gateTrigger.reset(); } void FMOp::onSampleRateChange() { _steps = modulationSteps; float sampleRate = engineGetSampleRate(); _envelope.setSampleRate(sampleRate); _phasor.setSampleRate(sampleRate); _decimator.setParams(sampleRate, oversample); _maxFrequency = 0.475f * sampleRate; _feedbackSL.setParams(sampleRate, 5.0f, 1.0f); _depthSL.setParams(sampleRate, 5.0f, 1.0f); _levelSL.setParams(sampleRate, 10.0f, 1.0f); _sustainSL.setParams(sampleRate, 1.0f, 1.0f); } json_t* FMOp::toJson() { json_t* root = json_object(); json_object_set_new(root, LINEAR_LEVEL, json_boolean(_linearLevel)); return root; } void FMOp::fromJson(json_t* root) { json_t* ll = json_object_get(root, LINEAR_LEVEL); if (ll) { _linearLevel = json_is_true(ll); } } void FMOp::step() { if (!outputs[AUDIO_OUTPUT].active) { lights[ENV_TO_LEVEL_LIGHT].value = params[ENV_TO_LEVEL_PARAM].value > 0.5f; lights[ENV_TO_FEEDBACK_LIGHT].value = params[ENV_TO_FEEDBACK_PARAM].value > 0.5f; lights[ENV_TO_DEPTH_LIGHT].value = params[ENV_TO_DEPTH_PARAM].value > 0.5f; return; } lights[ENV_TO_LEVEL_LIGHT].value = _levelEnvelopeOn; lights[ENV_TO_FEEDBACK_LIGHT].value = _feedbackEnvelopeOn; lights[ENV_TO_DEPTH_LIGHT].value = _depthEnvelopeOn; float pitchIn = 0.0f; if (inputs[PITCH_INPUT].active) { pitchIn = inputs[PITCH_INPUT].value; } float gateIn = 0.0f; if (inputs[GATE_INPUT].active) { gateIn = inputs[GATE_INPUT].value; } ++_steps; if (_steps >= modulationSteps) { _steps = 0; float ratio = params[RATIO_PARAM].value; if (ratio < 0.0f) { ratio = std::max(1.0f + ratio, 0.01f); } else { ratio *= 9.0f; ratio += 1.0f; } float frequency = pitchIn; frequency += params[FINE_PARAM].value / 12.0f; frequency = cvToFrequency(frequency); frequency *= ratio; frequency = clamp(frequency, -_maxFrequency, _maxFrequency); _phasor.setFrequency(frequency / (float)oversample); bool levelEnvelopeOn = params[ENV_TO_LEVEL_PARAM].value > 0.5f; bool feedbackEnvelopeOn = params[ENV_TO_FEEDBACK_PARAM].value > 0.5f; bool depthEnvelopeOn = params[ENV_TO_DEPTH_PARAM].value > 0.5f; if (_levelEnvelopeOn != levelEnvelopeOn || _feedbackEnvelopeOn != feedbackEnvelopeOn || _depthEnvelopeOn != depthEnvelopeOn) { _levelEnvelopeOn = levelEnvelopeOn; _feedbackEnvelopeOn = feedbackEnvelopeOn; _depthEnvelopeOn = depthEnvelopeOn; bool envelopeOn = _levelEnvelopeOn || _feedbackEnvelopeOn || _depthEnvelopeOn; if (envelopeOn && !_envelopeOn) { _envelope.reset(); } _envelopeOn = envelopeOn; } if (_envelopeOn) { float sustain = params[SUSTAIN_PARAM].value; if (inputs[SUSTAIN_INPUT].active) { sustain *= clamp(inputs[SUSTAIN_INPUT].value / 10.0f, 0.0f, 1.0f); } _envelope.setAttack(powf(params[ATTACK_PARAM].value, 2.0f) * 10.f); _envelope.setDecay(powf(params[DECAY_PARAM].value, 2.0f) * 10.f); _envelope.setSustain(_sustainSL.next(sustain)); _envelope.setRelease(powf(params[RELEASE_PARAM].value, 2.0f) * 10.f); } _feedback = params[FEEDBACK_PARAM].value; if (inputs[FEEDBACK_INPUT].active) { _feedback *= clamp(inputs[FEEDBACK_INPUT].value / 10.0f, 0.0f, 1.0f); } _depth = params[DEPTH_PARAM].value; if (inputs[DEPTH_INPUT].active) { _depth *= clamp(inputs[DEPTH_INPUT].value / 10.0f, 0.0f, 1.0f); } _level = params[LEVEL_PARAM].value; if (inputs[LEVEL_INPUT].active) { _level *= clamp(inputs[LEVEL_INPUT].value / 10.0f, 0.0f, 1.0f); } } float envelope = 0.0f; if (_envelopeOn) { _gateTrigger.process(gateIn); _envelope.setGate(_gateTrigger.isHigh()); envelope = _envelope.next(); } float feedback = _feedbackSL.next(_feedback); if (_feedbackEnvelopeOn) { feedback *= envelope; } bool feedbackOn = feedback > 0.001f; float out = _levelSL.next(_level); if (_levelEnvelopeOn) { out *= envelope; } float offset = 0.0f; if (feedbackOn) { offset = feedback * _feedbackDelayedSample; } if (inputs[FM_INPUT].active) { float depth = _depthSL.next(_depth); if (_depthEnvelopeOn) { depth *= envelope; } offset += inputs[FM_INPUT].value * depth * 2.0f; } float sample = 0.0f; if (out > 0.0001f) { Phasor::phase_delta_t o = Phasor::radiansToPhase(offset); if (feedbackOn) { if (_oversampleMix < 1.0f) { _oversampleMix += oversampleMixIncrement; } } else if (_oversampleMix > 0.0f) { _oversampleMix -= oversampleMixIncrement; } if (_oversampleMix > 0.0f) { for (int i = 0; i < oversample; ++i) { _phasor.advancePhase(); _buffer[i] = _sineTable.nextFromPhasor(_phasor, o); } sample = _oversampleMix * _decimator.next(_buffer); } else { _phasor.advancePhase(oversample); } if (_oversampleMix < 1.0f) { sample += (1.0f - _oversampleMix) * _sineTable.nextFromPhasor(_phasor, o); } if (_linearLevel) { sample *= out; } else { out = (1.0f - out) * Amplifier::minDecibels; _amplifier.setLevel(out); sample = _amplifier.next(sample); } } else { _phasor.advancePhase(oversample); } outputs[AUDIO_OUTPUT].value = _feedbackDelayedSample = amplitude * sample; } struct LinearLevelMenuItem : MenuItem { FMOp* _module; LinearLevelMenuItem(FMOp* module, const char* label) : _module(module) { this->text = label; } void onAction(EventAction &e) override { _module->_linearLevel = !_module->_linearLevel; } void step() override { rightText = _module->_linearLevel ? "✔" : ""; } }; struct FMOpWidget : ModuleWidget { static constexpr int hp = 10; FMOpWidget(FMOp* module) : ModuleWidget(module) { box.size = Vec(RACK_GRID_WIDTH * hp, RACK_GRID_HEIGHT); { SVGPanel *panel = new SVGPanel(); panel->box.size = box.size; panel->setBackground(SVG::load(assetPlugin(plugin, "res/FMOp.svg"))); addChild(panel); } addChild(Widget::create(Vec(15, 0))); addChild(Widget::create(Vec(box.size.x - 30, 0))); addChild(Widget::create(Vec(15, 365))); addChild(Widget::create(Vec(box.size.x - 30, 365))); // generated by svg_widgets.rb auto ratioParamPosition = Vec(30.0, 45.0); auto fineParamPosition = Vec(112.0, 57.0); auto attackParamPosition = Vec(107.0, 94.0); auto decayParamPosition = Vec(107.0, 139.0); auto sustainParamPosition = Vec(107.0, 184.0); auto releaseParamPosition = Vec(107.0, 229.0); auto depthParamPosition = Vec(36.0, 106.0); auto envToDepthParamPosition = Vec(59.0, 139.7); auto feedbackParamPosition = Vec(36.0, 162.0); auto envToFeedbackParamPosition = Vec(59.0, 195.7); auto levelParamPosition = Vec(36.0, 218.0); auto envToLevelParamPosition = Vec(59.0, 251.7); auto depthInputPosition = Vec(15.0, 274.0); auto feedbackInputPosition = Vec(47.0, 274.0); auto levelInputPosition = Vec(79.0, 274.0); auto sustainInputPosition = Vec(111.0, 274.0); auto pitchInputPosition = Vec(15.0, 318.0); auto fmInputPosition = Vec(47.0, 318.0); auto gateInputPosition = Vec(79.0, 318.0); auto audioOutputPosition = Vec(111.0, 318.0); auto envToDepthLightPosition = Vec(30.0, 141.0); auto envToFeedbackLightPosition = Vec(30.0, 197.0); auto envToLevelLightPosition = Vec(30.0, 253.0); // end generated by svg_widgets.rb addParam(ParamWidget::create(ratioParamPosition, module, FMOp::RATIO_PARAM, -1.0, 1.0, 0.0)); addParam(ParamWidget::create(fineParamPosition, module, FMOp::FINE_PARAM, -1.0, 1.0, 0.0)); addParam(ParamWidget::create(attackParamPosition, module, FMOp::ATTACK_PARAM, 0.0, 1.0, 0.12)); addParam(ParamWidget::create(decayParamPosition, module, FMOp::DECAY_PARAM, 0.0, 1.0, 0.31623)); addParam(ParamWidget::create(sustainParamPosition, module, FMOp::SUSTAIN_PARAM, 0.0, 1.0, 1.0)); addParam(ParamWidget::create(releaseParamPosition, module, FMOp::RELEASE_PARAM, 0.0, 1.0, 0.31623)); addParam(ParamWidget::create(depthParamPosition, module, FMOp::DEPTH_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(feedbackParamPosition, module, FMOp::FEEDBACK_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(levelParamPosition, module, FMOp::LEVEL_PARAM, 0.0, 1.0, 1.0)); addParam(ParamWidget::create(envToLevelParamPosition, module, FMOp::ENV_TO_LEVEL_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(envToFeedbackParamPosition, module, FMOp::ENV_TO_FEEDBACK_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(envToDepthParamPosition, module, FMOp::ENV_TO_DEPTH_PARAM, 0.0, 1.0, 0.0)); addInput(Port::create(sustainInputPosition, Port::INPUT, module, FMOp::SUSTAIN_INPUT)); addInput(Port::create(depthInputPosition, Port::INPUT, module, FMOp::DEPTH_INPUT)); addInput(Port::create(feedbackInputPosition, Port::INPUT, module, FMOp::FEEDBACK_INPUT)); addInput(Port::create(levelInputPosition, Port::INPUT, module, FMOp::LEVEL_INPUT)); addInput(Port::create(pitchInputPosition, Port::INPUT, module, FMOp::PITCH_INPUT)); addInput(Port::create(gateInputPosition, Port::INPUT, module, FMOp::GATE_INPUT)); addInput(Port::create(fmInputPosition, Port::INPUT, module, FMOp::FM_INPUT)); addOutput(Port::create(audioOutputPosition, Port::OUTPUT, module, FMOp::AUDIO_OUTPUT)); addChild(ModuleLightWidget::create>(envToLevelLightPosition, module, FMOp::ENV_TO_LEVEL_LIGHT)); addChild(ModuleLightWidget::create>(envToFeedbackLightPosition, module, FMOp::ENV_TO_FEEDBACK_LIGHT)); addChild(ModuleLightWidget::create>(envToDepthLightPosition, module, FMOp::ENV_TO_DEPTH_LIGHT)); } void appendContextMenu(Menu* menu) override { FMOp* fmop = dynamic_cast(module); assert(fmop); menu->addChild(new MenuLabel()); menu->addChild(new LinearLevelMenuItem(fmop, "Linear level response")); } }; RACK_PLUGIN_MODEL_INIT(Bogaudio, FMOp) { Model *modelFMOp = createModel("Bogaudio-FMOp", "FM-OP", "FM oscillator", OSCILLATOR_TAG, SYNTH_VOICE_TAG); return modelFMOp; }