#include "BaconPlugs.hpp" #include #include #include #include "dsp/digital.hpp" #include "KSSynth.hpp" namespace rack_plugin_BaconMusic { struct KarplusStrongPoly : virtual Module { enum ParamIds { INITIAL_PACKET, FILTER_TYPE, FREQ_KNOB, ATTEN_KNOB, FILTER_KNOB_A, FILTER_KNOB_B, FILTER_KNOB_C, NUM_PARAMS }; enum InputIds { TRIGGER_GATE, INITIAL_PACKET_INPUT, FILTER_INPUT, FREQ_CV, ATTEN_CV, FILTER_CV_A, FILTER_CV_B, FILTER_CV_C, NUM_INPUTS }; enum OutputIds { SYNTH_OUTPUT, NUM_OUTPUTS }; enum LightIds { LIGHT_PACKET_KNOB, LIGHT_PACKET_CV, LIGHT_FILTER_KNOB, LIGHT_FILTER_CV, LIGHT_FILTER_A, LIGHT_FILTER_B, LIGHT_FILTER_C, NUM_LIGHTS }; SchmittTrigger voiceTrigger; std::vector< KSSynth *> voices; const static int nVoices = 32; KarplusStrongPoly() : Module( NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS ) { for( int i=0; iinitPacketName( currentInitialPacket ); filterStringDirty = true; currentFilter = KSSynth::WEIGHTED_ONE_SAMPLE; filterString = voices[ 0 ]->filterTypeName( currentFilter ); } virtual ~KarplusStrongPoly() { for( auto syn : voices ) delete syn; } int getNumPackets() { return voices[ 0 ]->numInitPackets(); } KSSynth::InitPacket currentInitialPacket; int getNumFilters() { return voices[ 0 ]->numFilterTypes(); } KSSynth::FilterType currentFilter; void step() override { int nextInitialPacket = currentInitialPacket; float ipi, ipiS; lights[ LIGHT_PACKET_CV ].value = inputs[INITIAL_PACKET_INPUT].active; lights[ LIGHT_PACKET_KNOB ].value = !inputs[INITIAL_PACKET_INPUT].active; lights[ LIGHT_FILTER_CV ].value = inputs[FILTER_INPUT].active; lights[ LIGHT_FILTER_KNOB ].value = !inputs[FILTER_INPUT].active; // For now, since we only have one filter, hardcode this lights[ LIGHT_FILTER_A ].value = 1; lights[ LIGHT_FILTER_B ].value = 0; lights[ LIGHT_FILTER_C ].value = 0; if( inputs[ INITIAL_PACKET_INPUT ].active ) { ipi = inputs[ INITIAL_PACKET_INPUT ].value; ipiS = ipi * getNumPackets() / 10.0; nextInitialPacket = (int)(ipiS); } else { nextInitialPacket = (int)( params[ INITIAL_PACKET ].value ); } if( nextInitialPacket != currentInitialPacket ) { initPacketStringDirty = true; currentInitialPacket = (KSSynth::InitPacket)( nextInitialPacket ); initPacketString = voices[ 0 ]->initPacketName( currentInitialPacket ); } // Check a trigger here and find a voice bool newVoice = false; if( voiceTrigger.process( inputs[ TRIGGER_GATE ].value ) ) { newVoice = true; } if( newVoice ) { // find voice KSSynth *voice = NULL; for( auto syn: voices ) if( ! syn->active ) { voice = syn; break; } if( voice == NULL ) { // info( "All voices are active: Running voice steal" ); voice = voices[ 0 ]; float ds = voice->sumDelaySquared; for( auto syn: voices ) { if( syn->sumDelaySquared < ds ) { ds = syn->sumDelaySquared; voice = syn; } } } // Capture parameters onto this voice and trigger it float pitch = params[ FREQ_KNOB ].value + 12.0f * inputs[ FREQ_CV ].value; float freq = 261.262f * powf( 2.0f, pitch / 12.0f ); // For now, since we only have one filter, hardcode this voice->filtParamA = clamp( params[ FILTER_KNOB_A ].value + inputs[ FILTER_CV_A ].value * 0.1, 0.0f, 1.0f ); voice->filtParamB = 0; voice->filtParamC = 0; float atten = params[ ATTEN_KNOB ].value + inputs[ ATTEN_CV ].value; voice->packet = currentInitialPacket; voice->filtAtten = atten; voice->trigger( freq ); } float out = 0.0f; for( auto syn : voices ) if( syn->active ) out += syn->step(); outputs[ SYNTH_OUTPUT ].value = out; } bool initPacketStringDirty; std::string initPacketString; static bool getInitialPacketStringDirty( Module *that ) { return dynamic_cast(that)->initPacketStringDirty; } static std::string getInitialPacketString( Module *that ) { dynamic_cast(that)->initPacketStringDirty = false; return dynamic_cast(that)->initPacketString; } bool filterStringDirty; std::string filterString; static bool getFilterStringDirty( Module *that ) { return dynamic_cast(that)->filterStringDirty; } static std::string getFilterString( Module *that ) { dynamic_cast(that)->filterStringDirty = false; return dynamic_cast(that)->filterString; } }; struct KarplusStrongPolyWidget : ModuleWidget { KarplusStrongPolyWidget( KarplusStrongPoly *module); }; KarplusStrongPolyWidget::KarplusStrongPolyWidget( KarplusStrongPoly *module ) : ModuleWidget( module ) { box.size = Vec( SCREW_WIDTH * 15, RACK_HEIGHT ); BaconBackground *bg = new BaconBackground( box.size, "KarplusStrongPoly" ); addChild( bg->wrappedInFramebuffer()); float outy; float yh; int margin = 4; float gap = 13; int obuf = 10; outy = 35; float scale = 1.0; bool last = false; auto brd = [&](float ys) { // Add a downward pointing triangle here which means I need a draw glyph if( ! last ) { int w = 70; addChild( new BufferedDrawLambdaWidget( Vec( bg->cx() - w/2, outy + ys + margin ), Vec( w, gap ), [=](NVGcontext *vg) { nvgBeginPath( vg ); nvgMoveTo( vg, 0, 0 ); nvgLineTo( vg, w/2, gap ); nvgLineTo( vg, w, 0 ); nvgClosePath( vg ); nvgStrokeColor( vg, COLOR_BLACK ); nvgStroke( vg ); nvgFillColor( vg, nvgRGB( 240 * scale, 240 * scale, 200 * scale ) ); nvgFill( vg ); } ) ); } bg->addRoundedBorder( Vec( obuf, outy - margin ), Vec( box.size.x - 2 * obuf, ys + 2 * margin ), nvgRGB( 240*scale, 240*scale, 200*scale ) ); scale *= 0.92; }; auto cl = [&](std::string lab, float ys) { bg->addLabel( Vec( obuf + margin, outy + ys / 2 ), lab.c_str(), 13, NVG_ALIGN_MIDDLE | NVG_ALIGN_LEFT ); }; yh = SizeTable::Y; brd( yh ); cl( "Trigger", yh ); addInput( Port::create< PJ301MPort >( Vec( box.size.x - obuf - margin - SizeTable::X, outy ), Port::INPUT, module, KarplusStrongPoly::TRIGGER_GATE ) ); outy += yh + 2 * margin + gap; yh = SizeTable::Y; brd( yh ); cl( "Freq", yh ); int xp = box.size.x - margin - obuf - SizeTable::X; addInput( Port::create< PJ301MPort >( Vec( xp, outy + diffY2c< RoundBlackKnob, PJ301MPort >() ), Port::INPUT, module, KarplusStrongPoly::FREQ_CV ) ); xp -= SizeTable::X + margin; addParam( ParamWidget::create< RoundBlackKnob >( Vec( xp, outy ), module, KarplusStrongPoly::FREQ_KNOB, -54.0f, 54.0f, 0.0f ) ); outy += yh + 2 * margin + gap; yh = SizeTable::Y; brd( yh ); cl( "Packet", yh ); xp = 55; addChild( ModuleLightWidget::create< SmallLight< BlueLight > >( Vec( xp - 2, outy - 2 ), module, KarplusStrongPoly::LIGHT_PACKET_KNOB ) ); addParam( ParamWidget::create< RoundBlackSnapKnob >( Vec( xp, outy ), module, KarplusStrongPoly::INITIAL_PACKET, 0, module->getNumPackets()-1, 0 ) ); xp += SizeTable::X + margin; addChild( ModuleLightWidget::create< SmallLight< BlueLight > >( Vec( xp - 2, outy - 2 + diffY2c()), module, KarplusStrongPoly::LIGHT_PACKET_CV ) ); addInput( Port::create( Vec( xp, outy + diffY2c() ), Port::INPUT, module, KarplusStrongPoly::INITIAL_PACKET_INPUT ) ); xp += SizeTable::X + margin; addChild( DotMatrixLightTextWidget::create( Vec( xp, outy + diffY2c() ), module, 8, KarplusStrongPoly::getInitialPacketStringDirty, KarplusStrongPoly::getInitialPacketString ) ); outy += yh + 2 * margin + gap; yh = SizeTable::Y + SizeTable::Y + margin; brd( yh ); cl( "Filter", SizeTable::Y ); xp = 55; addChild( ModuleLightWidget::create< SmallLight< BlueLight > >( Vec( xp - 2, outy - 2 ), module, KarplusStrongPoly::LIGHT_FILTER_KNOB ) ); addParam( ParamWidget::create< RoundBlackSnapKnob >( Vec( xp, outy ), module, KarplusStrongPoly::FILTER_TYPE, 0, module->getNumFilters()-1, 0 ) ); xp += SizeTable::X + margin; addChild( ModuleLightWidget::create< SmallLight< BlueLight > >( Vec( xp - 2, outy - 2 + diffY2c()), module, KarplusStrongPoly::LIGHT_FILTER_CV ) ); addInput( Port::create( Vec( xp, outy + diffY2c() ), Port::INPUT, module, KarplusStrongPoly::FILTER_INPUT ) ); xp += SizeTable::X + margin; addChild( DotMatrixLightTextWidget::create( Vec( xp, outy + diffY2c() ), module, 8, KarplusStrongPoly::getFilterStringDirty, KarplusStrongPoly::getFilterString ) ); outy += SizeTable::Y + 2 * margin; xp = obuf + 2.5 * margin; for( int i=0; i<3; ++i ) { addChild( ModuleLightWidget::create< SmallLight< BlueLight> >( Vec( xp - 2, outy - 2 ), module, KarplusStrongPoly::LIGHT_FILTER_A + i ) ); bg->addLabel( Vec( xp, outy + SizeTable::Y ), i == 0 ? "A" : i == 1 ? "B" : "C", 12, NVG_ALIGN_BOTTOM | NVG_ALIGN_RIGHT ); xp += 3; addParam( ParamWidget::create< RoundSmallBlackKnob >( Vec( xp, outy ), module, KarplusStrongPoly::FILTER_KNOB_A + i, 0, 1, 0.5 ) ); xp += SizeTable::X + margin; addInput( Port::create( Vec( xp, outy + diffY2c() ), Port::INPUT, module, KarplusStrongPoly::FILTER_CV_A + i ) ); xp += SizeTable::X + 3.5 * margin; } outy += yh - SizeTable::Y + gap; yh = SizeTable< RoundBlackKnob >::Y; brd( yh ); cl( "Atten", yh ); xp = box.size.x - margin - obuf - SizeTable::X; addInput( Port::create< PJ301MPort >( Vec( xp, outy + diffY2c< RoundBlackKnob, PJ301MPort >() ), Port::INPUT, module, KarplusStrongPoly::ATTEN_CV ) ); xp -= SizeTable::X + margin; addParam( ParamWidget::create< RoundBlackKnob >( Vec( xp, outy ), module, KarplusStrongPoly::ATTEN_KNOB, 0.1, 4, 1.95 ) ); outy += yh + 2 * margin + gap; last = true; brd( SizeTable::Y ); cl( "Output", SizeTable::Y ); addOutput( Port::create< PJ301MPort >( Vec( box.size.x - obuf - margin - SizeTable::X, outy ), Port::OUTPUT, module, KarplusStrongPoly::SYNTH_OUTPUT ) ); } } // namespace rack_plugin_BaconMusic using namespace rack_plugin_BaconMusic; RACK_PLUGIN_MODEL_INIT(BaconMusic, KarplusStrongPoly) { Model *modelKarplusStrongPoly = Model::create("Bacon Music", "KarplusStrongPoly", "KarplusStrongPoly", OSCILLATOR_TAG ); return modelKarplusStrongPoly; }