diff --git a/plugin.json b/plugin.json index b049358..7a7c60d 100644 --- a/plugin.json +++ b/plugin.json @@ -92,6 +92,14 @@ "Sequencer", "Quantizer" ] + }, + { + "slug": "Ouros", + "name": "Ouros", + "description": "A experimental phased-feedback stereo oscillator, with display.", + "tags": [ + "Oscillator" + ] }, { "slug": "Magnets", diff --git a/res/EnvelopeArray-dark.svg b/res/EnvelopeArray-dark.svg index 1ba42a1..0df0b58 100644 --- a/res/EnvelopeArray-dark.svg +++ b/res/EnvelopeArray-dark.svg @@ -173,8 +173,8 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="1.6837088" - inkscape:cx="177.28719" - inkscape:cy="225.69223" + inkscape:cx="100.07669" + inkscape:cy="201.93516" inkscape:window-width="1392" inkscape:window-height="1027" inkscape:window-x="336" @@ -193,8 +193,8 @@ x="-0.31299999" y="-0.366" width="216.463" - height="364.25201" - style="fill:#202020" /> + height="366.03763" + style="fill:#202020;stroke-width:1" /> + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml diff --git a/res/Ouros.svg b/res/Ouros.svg new file mode 100644 index 0000000..24e749c --- /dev/null +++ b/res/Ouros.svg @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml diff --git a/src/Collatz.cpp b/src/Collatz.cpp index e866330..e823c83 100644 --- a/src/Collatz.cpp +++ b/src/Collatz.cpp @@ -98,176 +98,174 @@ struct Collatz : Module { configLight(COMPLETION_LIGHT, "Completion Indicator"); } - // Additional method signatures - void process(const ProcessArgs& args) override; void advanceSequence(); -}; -void Collatz::process(const ProcessArgs& args) { - - // Calculate the potential starting number every cycle - float knobValue = params[START_NUMBER].getValue(); - float cvValue = inputs[START_NUMBER_CV].isConnected() ? - inputs[START_NUMBER_CV].getVoltage() : 0.0f; - cvValue *= params[START_NUMBER_ATT].getValue(); - int startingNumber = std::abs(static_cast((knobValue + 100*cvValue))); - - // Calculate the potential starting number every cycle - float beatModIN = params[BEAT_MODULUS].getValue(); - float beatModAtt = params[BEAT_MODULUS_ATT].getValue(); - float beatModCV = inputs[BEAT_MODULUS_CV].isConnected() ? - inputs[BEAT_MODULUS_CV].getVoltage() : 0.0f; - beatMod = std::abs(static_cast(beatModIN+beatModAtt*10*beatModCV)); + void process(const ProcessArgs &args) override { + + // Calculate the potential starting number every cycle + float knobValue = params[START_NUMBER].getValue(); + float cvValue = inputs[START_NUMBER_CV].isConnected() ? + inputs[START_NUMBER_CV].getVoltage() : 0.0f; + cvValue *= params[START_NUMBER_ATT].getValue(); + int startingNumber = std::abs(static_cast((knobValue + 100*cvValue))); + + // Calculate the potential starting number every cycle + float beatModIN = params[BEAT_MODULUS].getValue(); + float beatModAtt = params[BEAT_MODULUS_ATT].getValue(); + float beatModCV = inputs[BEAT_MODULUS_CV].isConnected() ? + inputs[BEAT_MODULUS_CV].getVoltage() : 0.0f; + beatMod = std::abs(static_cast(beatModIN+beatModAtt*10*beatModCV)); - if (currentNumber == 0){ - modNumber = startingNumber % beatMod; - steps = modNumber; - if (modNumber<1) {accents = 0;} //avoid divide by zero - else { accents = floor((currentNumber/modNumber) % beatMod);} - } - - // Display update logic - if (digitalDisplay) { - if (sequenceRunning) { - digitalDisplay->text = std::to_string(currentNumber) + " mod " + std::to_string(beatMod); - modNumber = currentNumber % beatMod; - steps = modNumber ; - if (modNumber<1) {accents = 0;} //avoid divide by zero - else { accents = floor((currentNumber/modNumber) % beatMod);} - - if (modNumberDisplay) { - std::string displayText = std::to_string(steps) + " : " + std::to_string(accents); - modNumberDisplay->text = displayText; - } - outputs[COMPLETION_OUTPUT].setVoltage(0.0f); - lights[COMPLETION_LIGHT].setBrightness(0); - } else { - digitalDisplay->text = std::to_string(startingNumber) + " mod " + std::to_string(beatMod); - modNumber = startingNumber % beatMod; - steps = modNumber; - if (modNumber<1) {//avoid divide by zero - accents = 0; - } else { - accents = floor((startingNumber/modNumber) % beatMod); - } - - if (modNumberDisplay) { - std::string displayText = std::to_string(steps) + " : " + std::to_string(accents) ; - modNumberDisplay->text = displayText; - } - - outputs[COMPLETION_OUTPUT].setVoltage(5.0f); - lights[COMPLETION_LIGHT].setBrightness(1); - } - } - - // Handle reset logic - if (resetTrigger.process(params[RESET_BUTTON_PARAM].getValue()) || - resetTrigger.process(inputs[RESET_INPUT].getVoltage()-0.01f)) { - sequenceRunning = false; - rhythmStepIndex = 0; - currentNumber = 0; - lights[RUN_LIGHT].setBrightness(0); - outputs[GATE_OUTPUT].setVoltage(0.0f); - outputs[ACCENT_OUTPUT].setVoltage(0.0f); - lights[GATE_LIGHT].setBrightness(0); - lights[ACCENT_LIGHT].setBrightness(0); - } - - // Handle trigger logic - if ( (sampleTrigger.process(inputs[START_INPUT].getVoltage()) || params[START_BUTTON_PARAM].getValue() > 0) && !sequenceRunning && !sequenceTriggered) { - sequenceTriggered = true; // Mark that a sequence start is pending - lights[RUN_LIGHT].setBrightness(1); - } - - // Clock handling logic - bool externalClockConnected = inputs[CLOCK_INPUT].isConnected(); - if (externalClockConnected && clockTrigger.process(inputs[CLOCK_INPUT].getVoltage()-0.01f)) { - if (sequenceTriggered) { - // Reset necessary variables for starting the sequence - currentNumber = startingNumber; - sequenceRunning = true; - sequenceTriggered = false; // Reset trigger flag after starting the sequence - rhythmStepIndex = 0; // Reset rhythm index if needed - } else if (sequenceRunning ) { - advanceSequence(); - } - - // Update lastClockTime for rate calculation - if (firstPulseReceived) {clockRate = 1.0f / lastClockTime;} - lastClockTime = 0.0f; - firstPulseReceived = true; - } - - // Accumulate time since the last clock pulse for rate calculation - if (firstPulseReceived && externalClockConnected) { - lastClockTime += args.sampleTime; - } - - // rhythm and output logic - if (sequenceRunning) { - steps = modNumber ; - if (modNumber<1) {accents = 0;} //avoid divide by zero - else { accents = floor((currentNumber/modNumber) % beatMod);} - - accumulatedTime += args.sampleTime; - accumulatedTimeB += args.sampleTime; - if (steps>=1){ //avoid divide by zero - float stepDuration = 1.0f / clockRate / steps; - if (accumulatedTime < stepDuration/2) { - gatePulse=5; - } else {gatePulse=0;} - if (accumulatedTime >= stepDuration) { - accumulatedTime -= stepDuration; - } - } else { - float stepDuration = 1.0f / clockRate / 1.0f; //avoid div by zero - if (accumulatedTime < stepDuration/2) { - gatePulse=5; - } else {gatePulse=0;} - if (accumulatedTime >= stepDuration) { - accumulatedTime -= stepDuration; - } - } - - if (accents>=1){ //avoid divide by zero - float accentDuration = 1.0f / clockRate / accents; - if (accumulatedTimeB < accentDuration/2) { - accentPulse=5; - } else {accentPulse=0;} - if (accumulatedTimeB >= accentDuration) { - accumulatedTimeB -= accentDuration; - } - } else { - float accentDuration = 1.0f / clockRate / 1.0f; - if (accumulatedTimeB < accentDuration/2) { - accentPulse=5; - } else {accentPulse=0;} - if (accumulatedTimeB >= accentDuration) { - accumulatedTimeB -= accentDuration; - } - } - - if (externalClockConnected){ - // Set gate and accent outputs - //for step or accents =0 suppress outputs - if (steps>=1){outputs[GATE_OUTPUT].setVoltage(gatePulse); - }else {outputs[GATE_OUTPUT].setVoltage(0.0f);} - if (accents>=1){outputs[ACCENT_OUTPUT].setVoltage(accentPulse); - }else {outputs[ACCENT_OUTPUT].setVoltage(0.0f);} - if (steps>=1){lights[GATE_LIGHT].setBrightness(gatePulse/5); - }else {lights[GATE_LIGHT].setBrightness(0);} - if (accents>=1){lights[ACCENT_LIGHT].setBrightness(accentPulse/5); - }else {lights[ACCENT_LIGHT].setBrightness(0);} - } else { - outputs[GATE_OUTPUT].setVoltage(0.0f); - outputs[ACCENT_OUTPUT].setVoltage(0.0f); - lights[GATE_LIGHT].setBrightness(0); - lights[ACCENT_LIGHT].setBrightness(0); - firstPulseReceived = false; - } - }// if (sequenceRunning) -}//void Collatz::process + if (currentNumber == 0){ + modNumber = startingNumber % beatMod; + steps = modNumber; + if (modNumber<1) {accents = 0;} //avoid divide by zero + else { accents = floor((currentNumber/modNumber) % beatMod);} + } + + // Display update logic + if (digitalDisplay) { + if (sequenceRunning) { + digitalDisplay->text = std::to_string(currentNumber) + " mod " + std::to_string(beatMod); + modNumber = currentNumber % beatMod; + steps = modNumber ; + if (modNumber<1) {accents = 0;} //avoid divide by zero + else { accents = floor((currentNumber/modNumber) % beatMod);} + + if (modNumberDisplay) { + std::string displayText = std::to_string(steps) + " : " + std::to_string(accents); + modNumberDisplay->text = displayText; + } + outputs[COMPLETION_OUTPUT].setVoltage(0.0f); + lights[COMPLETION_LIGHT].setBrightness(0); + } else { + digitalDisplay->text = std::to_string(startingNumber) + " mod " + std::to_string(beatMod); + modNumber = startingNumber % beatMod; + steps = modNumber; + if (modNumber<1) {//avoid divide by zero + accents = 0; + } else { + accents = floor((startingNumber/modNumber) % beatMod); + } + + if (modNumberDisplay) { + std::string displayText = std::to_string(steps) + " : " + std::to_string(accents) ; + modNumberDisplay->text = displayText; + } + + outputs[COMPLETION_OUTPUT].setVoltage(5.0f); + lights[COMPLETION_LIGHT].setBrightness(1); + } + } + + // Handle reset logic + if (resetTrigger.process(params[RESET_BUTTON_PARAM].getValue()) || + resetTrigger.process(inputs[RESET_INPUT].getVoltage()-0.01f)) { + sequenceRunning = false; + rhythmStepIndex = 0; + currentNumber = 0; + lights[RUN_LIGHT].setBrightness(0); + outputs[GATE_OUTPUT].setVoltage(0.0f); + outputs[ACCENT_OUTPUT].setVoltage(0.0f); + lights[GATE_LIGHT].setBrightness(0); + lights[ACCENT_LIGHT].setBrightness(0); + } + + // Handle trigger logic + if ( (sampleTrigger.process(inputs[START_INPUT].getVoltage()) || params[START_BUTTON_PARAM].getValue() > 0) && !sequenceRunning && !sequenceTriggered) { + sequenceTriggered = true; // Mark that a sequence start is pending + lights[RUN_LIGHT].setBrightness(1); + } + + // Clock handling logic + bool externalClockConnected = inputs[CLOCK_INPUT].isConnected(); + if (externalClockConnected && clockTrigger.process(inputs[CLOCK_INPUT].getVoltage()-0.01f)) { + if (sequenceTriggered) { + // Reset necessary variables for starting the sequence + currentNumber = startingNumber; + sequenceRunning = true; + sequenceTriggered = false; // Reset trigger flag after starting the sequence + rhythmStepIndex = 0; // Reset rhythm index if needed + } else if (sequenceRunning ) { + advanceSequence(); + } + + // Update lastClockTime for rate calculation + if (firstPulseReceived) {clockRate = 1.0f / lastClockTime;} + lastClockTime = 0.0f; + firstPulseReceived = true; + } + + // Accumulate time since the last clock pulse for rate calculation + if (firstPulseReceived && externalClockConnected) { + lastClockTime += args.sampleTime; + } + + // rhythm and output logic + if (sequenceRunning) { + steps = modNumber ; + if (modNumber<1) {accents = 0;} //avoid divide by zero + else { accents = floor((currentNumber/modNumber) % beatMod);} + + accumulatedTime += args.sampleTime; + accumulatedTimeB += args.sampleTime; + if (steps>=1){ //avoid divide by zero + float stepDuration = 1.0f / clockRate / steps; + if (accumulatedTime < stepDuration/2) { + gatePulse=5; + } else {gatePulse=0;} + if (accumulatedTime >= stepDuration) { + accumulatedTime -= stepDuration; + } + } else { + float stepDuration = 1.0f / clockRate / 1.0f; //avoid div by zero + if (accumulatedTime < stepDuration/2) { + gatePulse=5; + } else {gatePulse=0;} + if (accumulatedTime >= stepDuration) { + accumulatedTime -= stepDuration; + } + } + + if (accents>=1){ //avoid divide by zero + float accentDuration = 1.0f / clockRate / accents; + if (accumulatedTimeB < accentDuration/2) { + accentPulse=5; + } else {accentPulse=0;} + if (accumulatedTimeB >= accentDuration) { + accumulatedTimeB -= accentDuration; + } + } else { + float accentDuration = 1.0f / clockRate / 1.0f; + if (accumulatedTimeB < accentDuration/2) { + accentPulse=5; + } else {accentPulse=0;} + if (accumulatedTimeB >= accentDuration) { + accumulatedTimeB -= accentDuration; + } + } + + if (externalClockConnected){ + // Set gate and accent outputs + //for step or accents =0 suppress outputs + if (steps>=1){outputs[GATE_OUTPUT].setVoltage(gatePulse); + }else {outputs[GATE_OUTPUT].setVoltage(0.0f);} + if (accents>=1){outputs[ACCENT_OUTPUT].setVoltage(accentPulse); + }else {outputs[ACCENT_OUTPUT].setVoltage(0.0f);} + if (steps>=1){lights[GATE_LIGHT].setBrightness(gatePulse/5); + }else {lights[GATE_LIGHT].setBrightness(0);} + if (accents>=1){lights[ACCENT_LIGHT].setBrightness(accentPulse/5); + }else {lights[ACCENT_LIGHT].setBrightness(0);} + } else { + outputs[GATE_OUTPUT].setVoltage(0.0f); + outputs[ACCENT_OUTPUT].setVoltage(0.0f); + lights[GATE_LIGHT].setBrightness(0); + lights[ACCENT_LIGHT].setBrightness(0); + firstPulseReceived = false; + } + }// if (sequenceRunning) + }//void process +}; void Collatz::advanceSequence() { if (currentNumber <= 0) { diff --git a/src/HexMod.cpp b/src/HexMod.cpp index 8409b33..7175141 100644 --- a/src/HexMod.cpp +++ b/src/HexMod.cpp @@ -14,11 +14,11 @@ using namespace rack; -float linearInterpolate(float a, float b, float fraction) { - return a + fraction * (b - a); -} - struct HexMod : Module { + float linearInterpolate(float a, float b, float fraction) { + return a + fraction * (b - a); + } + enum ParamIds { RATE_KNOB, NODE_KNOB, @@ -88,8 +88,6 @@ struct HexMod : Module { float lfoPhase[6] = {0.0f}; // Current LFO phase for each channel float prevPhaseResetInput[6] = {}; // Previous envelope input, for peak detection - // Function declarations - void process(const ProcessArgs& args) override; float calculateTargetPhase(int channel, float NodePosition, float deltaTime, float place); void adjustLFOPhase(int channel, float targetPhase, float envInfluence, float deltaTime); void updateLEDs(int channel, float voltage); @@ -111,7 +109,6 @@ struct HexMod : Module { int SINprocessCounter = 0; // Counter to track process cycles int SkipProcesses = 4; //Number of process cycles to skip for the big calculation - float lastConnectedInputVoltage = 0.0f; float SyncInterval = 2; //default to 2hz // Serialization method to save module state @@ -196,7 +193,7 @@ struct HexMod : Module { } } - HexMod() { + HexMod() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); // Initialize knob parameters with a reasonable range and default values @@ -220,203 +217,197 @@ struct HexMod : Module { configOutput(LFO_OUTPUT_1 + i, "LFO " + std::to_string(i + 1)); } } -}; -void HexMod::process(const ProcessArgs& args) { + void process(const ProcessArgs &args) override { - float deltaTime = args.sampleTime; - LEDprocessCounter++; - SINprocessCounter++; + float deltaTime = args.sampleTime; + LEDprocessCounter++; + SINprocessCounter++; - //PROCESS INPUTS + //PROCESS INPUTS - // Calculate the rate from the RATE_KNOB and any RATE_INPUT CV - float rate = params[RATE_KNOB].getValue(); - if (inputs[RATE_INPUT].isConnected()) { - rate += inputs[RATE_INPUT].getVoltage()*params[RATE_ATT_KNOB].getValue(); // CV adds to the rate - } + // Calculate the rate from the RATE_KNOB and any RATE_INPUT CV + float rate = params[RATE_KNOB].getValue(); + if (inputs[RATE_INPUT].isConnected()) { + rate += inputs[RATE_INPUT].getVoltage()*params[RATE_ATT_KNOB].getValue(); // CV adds to the rate + } - if (voctEnabled){ - rate = inputs[RATE_INPUT].getVoltage(); - rate = clamp(rate, -10.f, 10.0f); - rate = 261.625565 * pow(2.0, rate); - } else { - rate = clamp(rate, 0.01f, 10.0f); - } - - // Calculate target phase based on Node knob - float NodePosition = params[NODE_KNOB].getValue(); - if (inputs[NODE_INPUT].isConnected()) { - NodePosition += inputs[NODE_INPUT].getVoltage()*params[NODE_ATT_KNOB].getValue(); // CV adds to the position - } - NodePosition = clamp(NodePosition, 0.0f, 3.0f); - - // Process clock sync input - float SyncInputVoltage; - - if (inputs[SYNC_INPUT].isConnected()) { - // Get the voltage from the SYNC input - SyncInputVoltage = inputs[SYNC_INPUT].getVoltage(); - - // Accumulate time in the timer - SyncTimer.process(args.sampleTime); - - // Check if the Sync Trigger condition is met - if (SyncTrigger.process(SyncInputVoltage)) { - if (!firstClockPulse){ - SyncInterval = SyncTimer.time; // Get the accumulated time since the last reset - SyncTimer.reset(); // Reset the timer for the next trigger interval measurement - - if (synclinkEnabled) { - clockSyncPulse = true; - } - } else { - SyncTimer.reset(); // Reset the timer for the next trigger interval measurement - firstClockPulse = false; - } - } - - if (syncEnabled) { - rate *= 1 / SyncInterval; // Rate knob becomes a multiplier when Sync is patched - } else { - rate = 1 / SyncInterval; // Rate knob is deactivated when Sync is patched - } - } - - - for (int i = 0; i < 6; i++) { - // Gate/trigger to Phase Reset input - float PhaseResetInput; - - // If the current input is connected, use it and update lastConnectedInputVoltage - if (inputs[ENV_INPUT_1 + i].isConnected()) { - PhaseResetInput = inputs[ENV_INPUT_1 + i].getVoltage(); - lastConnectedInputVoltage = PhaseResetInput; - } else { - // If not connected, use the last connected input's voltage - PhaseResetInput = lastConnectedInputVoltage; - } - - if (PhaseResetInput < 0.0001f){latch[i]= true; } - PhaseResetInput = clamp(PhaseResetInput, 0.0f, 10.0f); + if (voctEnabled){ + rate = inputs[RATE_INPUT].getVoltage(); + rate = clamp(rate, -10.f, 10.0f); + rate = 261.625565 * pow(2.0, rate); + } else { + rate = clamp(rate, 0.01f, 10.0f); + } + + // Calculate target phase based on Node knob + float NodePosition = params[NODE_KNOB].getValue(); + if (inputs[NODE_INPUT].isConnected()) { + NodePosition += inputs[NODE_INPUT].getVoltage()*params[NODE_ATT_KNOB].getValue(); // CV adds to the position + } + NodePosition = clamp(NodePosition, 0.0f, 3.0f); + + // Process clock sync input + float SyncInputVoltage; + + if (inputs[SYNC_INPUT].isConnected()) { + // Get the voltage from the SYNC input + SyncInputVoltage = inputs[SYNC_INPUT].getVoltage(); + + // Accumulate time in the timer + SyncTimer.process(args.sampleTime); + + // Check if the Sync Trigger condition is met + if (SyncTrigger.process(SyncInputVoltage)) { + if (!firstClockPulse){ + SyncInterval = SyncTimer.time; // Get the accumulated time since the last reset + SyncTimer.reset(); // Reset the timer for the next trigger interval measurement + + if (synclinkEnabled) { + clockSyncPulse = true; + } + } else { + SyncTimer.reset(); // Reset the timer for the next trigger interval measurement + firstClockPulse = false; + } + } + + if (syncEnabled) { + rate *= 1 / SyncInterval; // Rate knob becomes a multiplier when Sync is patched + } else { + rate = 1 / SyncInterval; // Rate knob is deactivated when Sync is patched + } + } + + + for (int i = 0; i < 6; i++) { + // Gate/trigger to Phase Reset input + float PhaseResetInput = 0.0f; + + if (inputs[ENV_INPUT_1 + i].isConnected()) { + PhaseResetInput = inputs[ENV_INPUT_1 + i].getVoltage(); + } + + if (PhaseResetInput < 0.01f){latch[i]= true; } + PhaseResetInput = clamp(PhaseResetInput, 0.0f, 10.0f); - // Check if the envelope is rising or falling with hysteresis - if (risingState[i]) { - // If it was rising, look for a significant drop before considering it falling - if (PhaseResetInput < prevPhaseResetInput[i]) { - risingState[i] = false; // Now it's falling - } - } else { - // If it was falling, look for a significant rise before considering it rising - if (PhaseResetInput > prevPhaseResetInput[i]) { - risingState[i] = true; // Now it's rising - lights[IN_LED_1+i].setBrightness(1.0f); - lights[OUT_LED_1a+i].setBrightness(1.0f); - lights[OUT_LED_1b+i].setBrightness(1.0f); - lights[OUT_LED_1c+i].setBrightness(1.0f); - lights[OUT_LED_1d+i].setBrightness(1.0f); - } - } - - float basePhase = i / -6.0f; // Starting with hexagonal distribution - float targetPhase = basePhase; // Default to base phase - - ///////////////////// - // NODE positioning logic - // - if (NodePosition < 1.0f) { - // Unison - targetPhase = linearInterpolate(basePhase, 0.5f, NodePosition); - } else if (NodePosition < 2.0f) { - // Bimodal distribution - float bimodalPhase = (i % 2) / 2.0f; - float dynamicFactor = -1.0f*(NodePosition - 1.0f)*((i+1.0f)/2.0f); - targetPhase = linearInterpolate(0.5f, bimodalPhase*dynamicFactor, NodePosition - 1.0f); - } else { - float bimodalPhase = (i % 2) / 2.0f; - float trimodalPhase = (i % 3) / 3.0f; - float blendFactor = NodePosition - 2.0f; // Gradually changes from 0 to 1 as NodePosition goes from 2.0 to 3.0 - float adjustedTrimodalPhase = trimodalPhase; - adjustedTrimodalPhase = linearInterpolate(bimodalPhase, trimodalPhase, blendFactor*1.0f ); - targetPhase = adjustedTrimodalPhase; - } - targetPhase += place[i]; - - while (targetPhase >= 1.0f) targetPhase -= 1.0f; - while (targetPhase < 0.0f) targetPhase += 1.0f; - - float phaseDiff = targetPhase - lfoPhase[i]; - // Ensure phaseDiff is within the -0.5 to 0.5 range to find the shortest path - if (phaseDiff > 0.5f) phaseDiff -= 1.0f; - if (phaseDiff < -0.5f) phaseDiff += 1.0f; - - if (synclinkEnabled){ - if (clockSyncPulse){ - lfoPhase[i] += phaseDiff; - } else { - lfoPhase[i] += phaseDiff*(0.2f*(rate/1000.f) ) - 0.199*pow((PhaseResetInput/10.0f),0.01f)*(rate/1000.f) ; - } - }else{ - //Phase returns to the correct spot, rate determined by PhaseGate - lfoPhase[i] += phaseDiff*(0.2f*(rate/1000.f) ) - 0.199*pow((PhaseResetInput/10.0f),0.01f)*(rate/1000.f) ; - } - - // Ensure phase is within [0, 1) - while (lfoPhase[i] >= 1.0f) lfoPhase[i] -= 1.0f; - while (lfoPhase[i] < 0.0f) lfoPhase[i] += 1.0f; - - // Update the LFO phase based on the rate - lfoPhase[i] += rate * deltaTime ; - if (lfoPhase[i] >= 1.0f) lfoPhase[i] -= 1.0f; // Wrap the phase - - place[i] += rate * deltaTime; + // Check if the envelope is rising or falling with hysteresis + if (risingState[i]) { + if (PhaseResetInput < prevPhaseResetInput[i]) { + risingState[i] = false; // Now it's falling + } + } else { + // If it was falling, look for a significant rise before considering it rising + if (PhaseResetInput > prevPhaseResetInput[i]) { + risingState[i] = true; // Now it's rising + lights[IN_LED_1+i].setBrightness(1.0f); + lights[OUT_LED_1a+i].setBrightness(1.0f); + lights[OUT_LED_1b+i].setBrightness(1.0f); + lights[OUT_LED_1c+i].setBrightness(1.0f); + lights[OUT_LED_1d+i].setBrightness(1.0f); + } + } + + float basePhase = i / -6.0f; // Starting with hexagonal distribution + float targetPhase = basePhase; // Default to base phase + + ///////////////////// + // NODE positioning logic + // + if (NodePosition < 1.0f) { + // Unison + targetPhase = linearInterpolate(basePhase, 0.5f, NodePosition); + } else if (NodePosition < 2.0f) { + // Bimodal distribution + float bimodalPhase = (i % 2) / 2.0f; + float dynamicFactor = -1.0f*(NodePosition - 1.0f)*((i+1.0f)/2.0f); + targetPhase = linearInterpolate(0.5f, bimodalPhase*dynamicFactor, NodePosition - 1.0f); + } else { + float bimodalPhase = (i % 2) / 2.0f; + float trimodalPhase = (i % 3) / 3.0f; + float blendFactor = NodePosition - 2.0f; // Gradually changes from 0 to 1 as NodePosition goes from 2.0 to 3.0 + float adjustedTrimodalPhase = trimodalPhase; + adjustedTrimodalPhase = linearInterpolate(bimodalPhase, trimodalPhase, blendFactor*1.0f ); + targetPhase = adjustedTrimodalPhase; + } + targetPhase += place[i]; + + while (targetPhase >= 1.0f) targetPhase -= 1.0f; + while (targetPhase < 0.0f) targetPhase += 1.0f; + + float phaseDiff = targetPhase - lfoPhase[i]; + // Ensure phaseDiff is within the -0.5 to 0.5 range to find the shortest path + if (phaseDiff > 0.5f) phaseDiff -= 1.0f; + if (phaseDiff < -0.5f) phaseDiff += 1.0f; + + if (synclinkEnabled){ + if (clockSyncPulse){ + lfoPhase[i] += phaseDiff; + } else { + lfoPhase[i] += phaseDiff*( ( 0.2f - 0.1999*(PhaseResetInput/10.0f) ) * (rate/1000.f) ) ; + } + }else{ + //Phase returns to the correct spot, rate determined by PhaseGate + lfoPhase[i] += phaseDiff*( ( 0.2f - 0.1999*(PhaseResetInput/10.0f) ) * (rate/1000.f) ) ; + } + + // Ensure phase is within [0, 1) + while (lfoPhase[i] >= 1.0f) lfoPhase[i] -= 1.0f; + while (lfoPhase[i] < 0.0f) lfoPhase[i] += 1.0f; + + // Update the LFO phase based on the rate + lfoPhase[i] += rate * deltaTime ; + if (lfoPhase[i] >= 1.0f) lfoPhase[i] -= 1.0f; // Wrap the phase + + place[i] += rate * deltaTime; - if (place[i] >= 1.0f) place[i] -= 1.0f; // Wrap - - // Reset LFO phase to 0 at the peak of the envelope - if ((risingState[i] && latch[i]) || (clockSyncPulse)) { - if(!clockSyncPulse){ - lfoPhase[i] = 0.0f; - } - place[i] = 0.0f; - latch[i]= false; - } - - float currentOutput = outputs[LFO_OUTPUT_1 + i].getVoltage(); - if (SINprocessCounter > SkipProcesses) { - // Generate LFO output using the sine function and the adjusted phase - lfoOutput[i] = 5.0f * sinf(2.0f * M_PI * lfoPhase[i]); - nextChunk[i] = lfoOutput[i]-currentOutput; - } - - // Since we process 1/N samples, linearly interpolate the rest - currentOutput += nextChunk[i] * 1/SkipProcesses; - - //Output Voltage - outputs[LFO_OUTPUT_1 + i].setVoltage(currentOutput); - if (lightsEnabled) { - if (LEDprocessCounter > 1500) { - // Update LEDs based on LFO output - updateLEDs(i, lfoOutput[i]); - - float brightness = lights[IN_LED_1+i].getBrightness(); - lights[IN_LED_1+i].setBrightness(brightness*0.9f); - lights[OUT_LED_1a+i].setBrightness(brightness*0.9f); - lights[OUT_LED_1b+i].setBrightness(brightness*0.9f); - lights[OUT_LED_1c+i].setBrightness(brightness*0.9f); - lights[OUT_LED_1d+i].setBrightness(brightness*0.9f); - } - } else { - for (int i = 0; i < NUM_LIGHTS; i++) {lights[i].setBrightness(0);} - } - - prevPhaseResetInput[i] = PhaseResetInput; - } + if (place[i] >= 1.0f) place[i] -= 1.0f; // Wrap + + // Reset LFO phase to 0 at the peak of the envelope + if ((risingState[i] && latch[i]) || (clockSyncPulse)) { + if(!clockSyncPulse){ + lfoPhase[i] = 0.0f; + } + place[i] = 0.0f; + latch[i]= false; + } + + float currentOutput = outputs[LFO_OUTPUT_1 + i].getVoltage(); + if (SINprocessCounter > SkipProcesses) { + // Generate LFO output using the sine function and the adjusted phase + lfoOutput[i] = 5.0f * sinf(2.0f * M_PI * lfoPhase[i]); + nextChunk[i] = lfoOutput[i]-currentOutput; + } + + // Since we process 1/N samples, linearly interpolate the rest + currentOutput += nextChunk[i] * 1/SkipProcesses; + + //Output Voltage + outputs[LFO_OUTPUT_1 + i].setVoltage(currentOutput); + if (lightsEnabled) { + if (LEDprocessCounter > 1500) { + // Update LEDs based on LFO output + updateLEDs(i, lfoOutput[i]); + + float brightness = lights[IN_LED_1+i].getBrightness(); + lights[IN_LED_1+i].setBrightness(brightness*0.9f); + lights[OUT_LED_1a+i].setBrightness(brightness*0.9f); + lights[OUT_LED_1b+i].setBrightness(brightness*0.9f); + lights[OUT_LED_1c+i].setBrightness(brightness*0.9f); + lights[OUT_LED_1d+i].setBrightness(brightness*0.9f); + } + } else { + for (int i = 0; i < NUM_LIGHTS; i++) {lights[i].setBrightness(0);} + } + + prevPhaseResetInput[i] = PhaseResetInput; + } - if (LEDprocessCounter > 1500) {LEDprocessCounter=0; } - if (SINprocessCounter > SkipProcesses) {SINprocessCounter=0; } - clockSyncPulse=false; -} + if (LEDprocessCounter > 1500) {LEDprocessCounter=0; } + if (SINprocessCounter > SkipProcesses) {SINprocessCounter=0; } + clockSyncPulse=false; + } +}; void HexMod::updateLEDs(int channel, float voltage) { // Ensure we do not exceed the array bounds @@ -541,8 +532,8 @@ struct HexModWidget : ModuleWidget { addInput(createInput(knobStartPos.plus(Vec(0.5*knobSpacing+2, 40)), module, HexMod::SYNC_INPUT)); } - - void appendContextMenu(Menu* menu) { + + void appendContextMenu(Menu* menu) override { ModuleWidget::appendContextMenu(menu); HexMod* hexMod = dynamic_cast(module); diff --git a/src/ImpulseController.cpp b/src/ImpulseController.cpp index dafcfc6..2248a49 100644 --- a/src/ImpulseController.cpp +++ b/src/ImpulseController.cpp @@ -140,9 +140,9 @@ struct ImpulseController : Module { configParam(SPREAD_PARAM, -1.0f, 1.f, 0.5f, "Spread"); configParam(DECAY_PARAM, 0.0f, 1.0f, 0.8f, "Decay"); - configParam(LAG_ATT_PARAM, -1.0f, 1.0f, 0.5f, "Lag Attenuverter"); - configParam(SPREAD_ATT_PARAM, -1.0f, 1.0f, 0.5f, "Spread Attenuverter"); - configParam(DECAY_ATT_PARAM, -1.0f, 1.0f, 0.5f, "Decay Attenuverter"); + configParam(LAG_ATT_PARAM, -1.0f, 1.0f, 0.0f, "Lag Attenuverter"); + configParam(SPREAD_ATT_PARAM, -1.0f, 1.0f, 0.0f, "Spread Attenuverter"); + configParam(DECAY_ATT_PARAM, -1.0f, 1.0f, 0.0f, "Decay Attenuverter"); configInput(_00_INPUT, "IN"); configInput(LAG_INPUT, "Lag"); @@ -163,7 +163,7 @@ struct ImpulseController : Module { } void process(const ProcessArgs& args) override { - const float baseSampleTime = 2.0f / 44100.0f; // Base sample time for 44.1 kHz + const float baseSampleTime = 2.0f / 44100.0f; // Base sample time to set chunk lengths const float ChunkLength = baseSampleTime/args.sampleTime; // Accumulate elapsed time diff --git a/src/Magnets.cpp b/src/Magnets.cpp index 2ad1955..3c31cf3 100644 --- a/src/Magnets.cpp +++ b/src/Magnets.cpp @@ -128,7 +128,7 @@ struct Magnets : Module { } }//Magnets() - void process(const ProcessArgs& args) { + void process(const ProcessArgs &args) override { // Read parameters and apply attenuations from CV inputs float temperature = params[TEMP_PARAM].getValue(); float polarization = params[POLARIZATION_PARAM].getValue(); diff --git a/src/Ouros.cpp b/src/Ouros.cpp new file mode 100644 index 0000000..16a4d94 --- /dev/null +++ b/src/Ouros.cpp @@ -0,0 +1,525 @@ +//////////////////////////////////////////////////////////// +// +// Ouros +// +// written by Cody Geary +// Copyright 2024, MIT License +// +// Stereo oscillator with phase-feedback +// +//////////////////////////////////////////////////////////// + +#include "rack.hpp" +#include "plugin.hpp" + +using namespace rack; + +template +class CircularBuffer { +private: + T buffer[Size]; + size_t index = 0; + +public: + CircularBuffer() { + // Initialize buffer to zero + std::fill(std::begin(buffer), std::end(buffer), T{}); + } + + void push(T value) { + buffer[index] = value; + index = (index + 1) % Size; + } + + T& operator[](size_t i) { + return buffer[(index + i) % Size]; + } + + const T& operator[](size_t i) const { + return buffer[(index + i) % Size]; + } + + static constexpr size_t size() { + return Size; + } +}; + +struct Ouros : Module { + + float linearInterpolation(float a, float b, float fraction) { + return a + fraction * (b - a); + } + + enum ParamIds { + RATE_KNOB, + NODE_KNOB, + ROTATE_KNOB, + SPREAD_KNOB, + FEEDBACK_KNOB, + MULTIPLY_KNOB, + RATE_ATT_KNOB, + NODE_ATT_KNOB, + ROTATE_ATT_KNOB, + SPREAD_ATT_KNOB, + FEEDBACK_ATT_KNOB, + FM_ATT_KNOB, + EAT_KNOB, + EAT_ATT_KNOB, + MULTIPLY_ATT_KNOB, + RESET_BUTTON, + NUM_PARAMS + }; + enum InputIds { + HARD_SYNC_INPUT, + RATE_INPUT, + NODE_INPUT, + ROTATE_INPUT, + SPREAD_INPUT, + FEEDBACK_INPUT, + FM_INPUT, + EAT_INPUT, + MULTIPLY_INPUT, + NUM_INPUTS + }; + enum OutputIds { + L_OUTPUT, + R_OUTPUT, + NUM_OUTPUTS + }; + enum LightIds { + + NUM_LIGHTS + }; + + // Initialize global variables + + dsp::SchmittTrigger SyncTrigger; + CircularBuffer waveBuffers[4]; + + float oscPhase[4] = {0.0f}; // Current oscillator phase for each channel + float prevPhaseResetInput = 0.0f; // Previous envelope input, for peak detection + float calculateTargetPhase(int channel, float NodePosition, float deltaTime, float place); + void adjustoscPhase(int channel, float targetPhase, float envInfluence, float deltaTime); + float lastTargetVoltages[4] = {0.f, 0.f, 0.f, 0.f}; // Initialize with default voltages, assuming start at 0V + float place[4] = {0.f, 0.f, 0.f, 0.f}; + bool risingState = false; // Initialize all channels as falling initially + bool latch = true; // Initialize all latches + float oscOutput[4] = {0.f, 0.f, 0.f, 0.f}; + float nextChunk[4] = {0.f, 0.f, 0.f, 0.f}; //measure next voltage step to subdivide + int LEDprocessCounter = 0; // Counter to track process cycles + int SINprocessCounter = 0; // Counter to track process cycles + float lastConnectedInputVoltage = 0.0f; + float SyncInterval = 2; //default to 2hz + float lastoscPhase[4] = {}; // Track the last phase for each LFO channel to detect wraps + float eatValue = 0.0f; + + Ouros() { + config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); + + // Initialize knob parameters with a reasonable range and default values + configParam(RATE_KNOB, -3.0f, 3.0f, 0.0f, "V/Oct offset"); // + configParam(NODE_KNOB, 0.0f, 5.0f, 0.0f, "Node Distribution"); // 0: Hexagonal, 1: Unison, 2: Bimodal, 3: Trimodal, 4: Unison, 5:Hexagonal + configParam(EAT_KNOB, -360.0f, 360.0f, 0.0f, "Feedback Position"); // + + configParam(ROTATE_KNOB, -360.0f, 360.0f, 0.0f, "Phase Rotation"); // + configParam(SPREAD_KNOB, -360.0f, 360.0f, 0.0f, "Stereo Phase Separation"); // + configParam(FEEDBACK_KNOB, -1.0f, 1.0f, 0.0f, "Feedback Amount"); // + configParam(MULTIPLY_KNOB, 1.0f, 10.0f, 1.0f, "Multiply Feedback Osc"); // + + configParam(NODE_ATT_KNOB, -1.0f, 1.0f, 0.0f, "Node Attenuation"); // + configParam(ROTATE_ATT_KNOB, -1.0f, 1.0f, 0.0f, "Rotate Attenuation"); // + configParam(SPREAD_ATT_KNOB, -1.0f, 1.0f, 0.0f, "Spread Attenuation"); // + configParam(FEEDBACK_ATT_KNOB, -1.0f, 1.0f, 0.0f, "Feedback Attenuation"); // + configParam(EAT_ATT_KNOB, -1.0f, 1.0f, 0.0f, "Feedback Position Attenuation"); // + configParam(MULTIPLY_ATT_KNOB, -1.0f, 1.0f, 0.0f, "Multiply Attenuation"); // + + configInput(ROTATE_INPUT, "Rotate"); + configInput(SPREAD_INPUT, "Phase Spread"); + configInput(FEEDBACK_INPUT, "Feedback"); + configInput(FM_INPUT, "FM"); + + configInput(RATE_INPUT, "V/Oct"); + configInput(NODE_INPUT, "Node Distribution"); + configInput(EAT_INPUT, "Feedback Position"); + configInput(MULTIPLY_INPUT, "Multiply"); + + configOutput(L_OUTPUT, "Orange Oscillator (L)" ); + configOutput(R_OUTPUT, "Blue Oscillator (R)" ); + + } + + void process(const ProcessArgs &args) override { + + float deltaTime = args.sampleTime; + + //PROCESS INPUTS + + // Calculate target phase based on Node knob + float fm = 0.0f; + if (inputs[FM_INPUT].isConnected()) { + fm += inputs[FM_INPUT].getVoltage()*0.2f*params[FM_ATT_KNOB].getValue(); + } + fm = clamp(fm, -2.0f, 2.0f); //limit FM to 1 octave + + float multiply = params[MULTIPLY_KNOB].getValue(); + if (inputs[MULTIPLY_INPUT].isConnected()) { + float multiplyIn = inputs[MULTIPLY_INPUT].getVoltage() * params[MULTIPLY_ATT_KNOB].getValue(); + if (multiplyIn < 0.0f){ + if ( (multiplyIn + multiply) < 1.0 ){ + multiply = 1-0.1f*(multiplyIn + multiply); + } else { + multiply +=multiplyIn; + } + } else { + multiply += multiplyIn; + } + } + multiply = clamp(multiply, 0.000001f, 10.0f); + + // Extract the integer part and the fractional part of multiply + float baseMultiple = int(multiply); + float remainder = multiply - baseMultiple; + + // Apply the non-linear adjustment based on the remainder + if (remainder < 0.5f) { + // If the remainder is less than 0.5, enhance its contribution non-linearly + multiply = baseMultiple + pow(remainder, 5.f); + } else { + // If the remainder is 0.5 or greater, non-linearly approach the next integer + multiply = (baseMultiple + 1) - pow(1.0f - remainder, 5.f); + } + + float rate = params[RATE_KNOB].getValue(); + if (inputs[RATE_INPUT].isConnected()) { + rate += inputs[RATE_INPUT].getVoltage(); + } + rate += fm; //add the FM to the computed rate + rate = clamp(rate, -3.0f, 3.0f); + + rate = 261.625565 * pow(2.0, rate); + + float multi_rate = rate*multiply; + + float rotate = params[ROTATE_KNOB].getValue(); + if (inputs[ROTATE_INPUT].isConnected()) { + rotate += inputs[ROTATE_INPUT].getVoltage() * 36.0f * params[ROTATE_ATT_KNOB].getValue(); + } + + float spread = params[SPREAD_KNOB].getValue(); + if (inputs[SPREAD_INPUT].isConnected()) { + spread += inputs[SPREAD_INPUT].getVoltage() * 36.0f * params[SPREAD_ATT_KNOB].getValue(); + } + + float eat = params[EAT_KNOB].getValue(); + if (inputs[EAT_INPUT].isConnected()) { + eat += inputs[EAT_INPUT].getVoltage() * 36.0f * params[EAT_ATT_KNOB].getValue(); + } + + float feedback = params[FEEDBACK_KNOB].getValue(); + if (inputs[FEEDBACK_INPUT].isConnected()) { + feedback += inputs[FEEDBACK_INPUT].getVoltage() * 0.1f * params[FEEDBACK_ATT_KNOB].getValue(); + } + feedback = clamp(feedback, -1.0f, 1.0f); + + // Calculate target phase based on Node knob + float NodePosition = params[NODE_KNOB].getValue(); + if (inputs[NODE_INPUT].isConnected()) { + NodePosition += inputs[NODE_INPUT].getVoltage()*params[NODE_ATT_KNOB].getValue(); + } + + NodePosition += feedback*oscOutput[3]; + NodePosition = fmod(NodePosition, 5.0f); + NodePosition = clamp(NodePosition, 0.0f, 5.0f); + + + // Gate/trigger to Phase Reset input + float PhaseResetInput=0.0f; + + bool manualResetPressed = params[RESET_BUTTON].getValue() > 0.0f; + + // If the current input is connected, use it and update lastConnectedInputVoltage + if (inputs[HARD_SYNC_INPUT].isConnected() || manualResetPressed) { + PhaseResetInput = inputs[HARD_SYNC_INPUT].getVoltage() + params[RESET_BUTTON].getValue(); + lastConnectedInputVoltage = PhaseResetInput; + } else { + lastConnectedInputVoltage = PhaseResetInput; + } + + if (PhaseResetInput < 0.0001f){latch= true; } + PhaseResetInput = clamp(PhaseResetInput, 0.0f, 10.0f); + + // Check if the envelope is rising or falling with hysteresis + if (risingState) { + // If it was rising, look for a significant drop before considering it falling + if (PhaseResetInput < prevPhaseResetInput) { + risingState = false; // Now it's falling + } + } else { + // If it was falling, look for a significant rise before considering it rising + if (PhaseResetInput > prevPhaseResetInput) { + risingState = true; // Now it's rising + } + } + + for (int i = 0; i < 4; i++) { + + ///////////////////// + // NODE positioning logic + // + + float nodeOne = (rotate+spread/2)/360; + float nodeTwo = (rotate-spread/2)/360; + float nodeThree = eat/360; + float currentNode = 0.0; + if (i==0){currentNode = nodeOne;} + if (i==1){currentNode = nodeTwo;} + if (i==3){currentNode = nodeThree;} + + float basePhase = currentNode; + float targetPhase = basePhase; + + if (NodePosition < 1.0f) { + // Unison + targetPhase = linearInterpolation(basePhase, 0.5f, NodePosition); + } else if (NodePosition < 2.0f) { + // Bimodal distribution + float bimodalPhase = fmod(currentNode, 2.0f) / 2.0f; + float dynamicFactor = -1.0f * (NodePosition - 1.0f) * ((currentNode + 1.0f) / 2.0f); + targetPhase = linearInterpolation(0.5f, bimodalPhase * dynamicFactor, NodePosition - 1.0f); + } else if (NodePosition < 3.0f) { + // Trimodal distribution + float bimodalPhase = fmod(currentNode, 2.0f) / 2.0f; + float dynamicFactor = -1.0f * (NodePosition - 1.0f) * ((currentNode + 1.0f) / 2.0f); + float trimodalPhase = fmod(currentNode, 3.0f) / 3.0f; + + float blendFactor = NodePosition - 2.0f; // Gradually changes from 0 to 1 as NodePosition goes from 2.0 to 3.0 + float adjustedTrimodalPhase = linearInterpolation(bimodalPhase * dynamicFactor, trimodalPhase, blendFactor * 1.0f); + targetPhase = adjustedTrimodalPhase; + } else if (NodePosition < 4.0f) { + float trimodalPhase = fmod(currentNode, 3.0f) / 3.0f; + + // Smoothly map back to Unison + float blendFactor = NodePosition - 3.0f; // Gradually changes from 0 to 1 as NodePosition goes from 3.0 to 4.0 + targetPhase = linearInterpolation(trimodalPhase, 0.5f, blendFactor); + } else { + // Map smoothly to the basePhase for 4-5 + float blendFactor = NodePosition - 4.0f; // Gradually changes from 0 to 1 as NodePosition goes from 4.0 to 5.0 + targetPhase = linearInterpolation(0.5f, basePhase, blendFactor); + } + + targetPhase += place[i]; + + if (i==2){ + targetPhase = place [i]; + } + + while (targetPhase >= 1.0f) targetPhase -= 1.0f; + while (targetPhase < 0.0f) targetPhase += 1.0f; + + float phaseDiff = targetPhase - oscPhase[i]; + // Ensure phaseDiff is within the -0.5 to 0.5 range to find the shortest path + if (phaseDiff > 0.5f) phaseDiff -= 1.0f; + if (phaseDiff < -0.5f) phaseDiff += 1.0f; + + //Phase returns to the correct spot, rate determined by PhaseGate + oscPhase[i] += phaseDiff*( 0.05f ) ; + + // Ensure phase is within [0, 1) + while (oscPhase[i] >= 1.0f) oscPhase[i] -= 1.0f; + while (oscPhase[i] < 0.0f) oscPhase[i] += 1.0f; + + if (i==3){ + // Update the LFO phase based on the rate + oscPhase[i] += multi_rate * deltaTime ; + // Ensure phase is within [0, 1) + while (oscPhase[i] >= 1.0f) oscPhase[i] -= 1.0f; + while (oscPhase[i] < 0.0f) oscPhase[i] += 1.0f; + + place[i] += multi_rate * deltaTime; + + if (oscPhase[2]==0){ + oscPhase[3]=0; + place[3]=0; + } + } else { + // Update the LFO phase based on the rate + oscPhase[i] += rate * deltaTime ; + // Ensure phase is within [0, 1) + while (oscPhase[i] >= 1.0f) oscPhase[i] -= 1.0f; + while (oscPhase[i] < 0.0f) oscPhase[i] += 1.0f; + + place[i] += rate * deltaTime; + } + + if (place[i] >= 1.0f) place[i] -= 1.0f; // Wrap + + // Reset LFO phase to 0 at the peak of the envelope + if ((risingState && latch) ) { + oscPhase[0] = 0.0f; + place[0] = 0.0f; + oscPhase[1] = 0.0f; + place[1] = 0.0f; + oscPhase[2] = 0.0f; + place[2] = 0.0f; + latch= false; + place[3] = 0.0f; + oscPhase[3] = 0.0f; + } + + //////////// + //COMPUTE the Oscillator Shape + oscOutput[i] = 5.0f * sinf(2.0f * M_PI * oscPhase[i]); + + if (i<2){ + //Output Voltage + outputs[L_OUTPUT + i].setVoltage(oscOutput[i]); + } + + prevPhaseResetInput = PhaseResetInput; + } + + int sampleIndex = static_cast(oscPhase[2] * 1024); + sampleIndex = std::max(0, std::min(sampleIndex, 1023)); + waveBuffers[0][sampleIndex] = outputs[L_OUTPUT].getVoltage(); + waveBuffers[1][sampleIndex] = outputs[R_OUTPUT].getVoltage(); + lastoscPhase[2] = oscPhase[2]; + + // Handling for wrapping around 0 + for (int i = 0; i < 4; i++) { + if (oscPhase[i] < lastoscPhase[i]) { // This means the phase has wrapped + lastoscPhase[i] = oscPhase[i]; // Update the last phase + } + } + + }//void process +}; + +struct PolarXYDisplay : TransparentWidget { + Ouros* module; + float centerX, centerY; + + Vec previousL = Vec(0.0f, 0.0f); + Vec previousR = Vec(0.0f, 0.0f); + + void draw(const DrawArgs& args) override { + if (!module) return; + + // Coordinates for the circle's center + centerX = box.size.x / 2.0f; + centerY = box.size.y / 2.0f; + + float xScale = 2 * M_PI / 1023; // Scale to fit the circular path + float radiusScale = centerY / 5; + + // Draw waveform from waveBuffers[0] + nvgBeginPath(args.vg); + for (size_t i = 0; i < 1024; i++) { + float theta = i * xScale; // Angle based on index + float radius = module->waveBuffers[0][i] * radiusScale + centerY; // Adjust radius based on sample value + Vec pos = polarToCartesian(theta, radius); + + if (i == 0) nvgMoveTo(args.vg, pos.x, pos.y); + else nvgLineTo(args.vg, pos.x, pos.y); + } + + nvgStrokeColor(args.vg, nvgRGBAf(1, .4, 0, 0.8)); // Drawing color for waveform 1 + nvgStrokeWidth(args.vg, 1.0); + nvgStroke(args.vg); + + // Draw waveform from waveBuffers[1] + nvgBeginPath(args.vg); + for (size_t i = 0; i < 1024; i++) { + float theta = i * xScale; // Angle based on index + float radius = module->waveBuffers[1][i] * radiusScale + centerY; // Adjust radius based on sample value + Vec pos = polarToCartesian(theta, radius); + + if (i == 0) nvgMoveTo(args.vg, pos.x, pos.y); + else nvgLineTo(args.vg, pos.x, pos.y); + } + + nvgStrokeColor(args.vg, nvgRGBAf(0, .4, 1, 0.8)); // Drawing color for waveform 2 + nvgStrokeWidth(args.vg, 1.0); + nvgStroke(args.vg); + } + + Vec polarToCartesian(float theta, float radius) { + float x = centerX + radius * cosf(theta); + float y = centerY + radius * sinf(theta); + return Vec(x, y); + } + + void drawLine(const DrawArgs& args, Vec fromPos, Vec toPos, NVGcolor color) { + nvgStrokeColor(args.vg, color); + nvgBeginPath(args.vg); + nvgMoveTo(args.vg, fromPos.x, fromPos.y); + nvgLineTo(args.vg, toPos.x, toPos.y); + nvgStrokeWidth(args.vg, 1.5); + nvgStroke(args.vg); + } +}; + +struct OurosWidget : ModuleWidget { + OurosWidget(Ouros* module) { + setModule(module); + + setPanel(createPanel( + asset::plugin(pluginInstance, "res/Ouros.svg"), + asset::plugin(pluginInstance, "res/Ouros-dark.svg") + )); + + // Add screws or additional design elements as needed + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + // Row of knobs at the bottom, with attenuators and CV inputs + const Vec knobStartPos = Vec(30, 165); + const float knobSpacing = 50.5f; + + addParam(createParamCentered (knobStartPos.plus(Vec( 0*knobSpacing, -25 )), module, Ouros::RESET_BUTTON)); + addInput(createInputCentered (knobStartPos.plus(Vec( 0*knobSpacing, 0 )), module, Ouros::HARD_SYNC_INPUT)); + + addParam(createParamCentered (knobStartPos.plus(Vec( 0*knobSpacing, 40 )), module, Ouros::FM_ATT_KNOB)); + addInput(createInputCentered (knobStartPos.plus(Vec( 0*knobSpacing, 65 )), module, Ouros::FM_INPUT)); + + addParam(createParamCentered(knobStartPos.plus(Vec( 1*knobSpacing, 0 )), module, Ouros::ROTATE_KNOB)); + addParam(createParamCentered (knobStartPos.plus(Vec( 1*knobSpacing, 30 )), module, Ouros::ROTATE_ATT_KNOB)); + addInput(createInputCentered (knobStartPos.plus(Vec( 1*knobSpacing, 55 )), module, Ouros::ROTATE_INPUT)); + + addParam(createParamCentered(knobStartPos.plus(Vec( 2*knobSpacing, 0 )), module, Ouros::SPREAD_KNOB)); + addParam(createParamCentered (knobStartPos.plus(Vec( 2*knobSpacing, 30 )), module, Ouros::SPREAD_ATT_KNOB)); + addInput(createInputCentered (knobStartPos.plus(Vec( 2*knobSpacing, 55 )), module, Ouros::SPREAD_INPUT)); + + addParam(createParamCentered(knobStartPos.plus(Vec( 3*knobSpacing, 0 )), module, Ouros::MULTIPLY_KNOB)); + addParam(createParamCentered (knobStartPos.plus(Vec( 3*knobSpacing, 30 )), module, Ouros::MULTIPLY_ATT_KNOB)); + addInput(createInputCentered (knobStartPos.plus(Vec( 3*knobSpacing, 55 )), module, Ouros::MULTIPLY_INPUT)); + + addParam(createParamCentered(knobStartPos.plus(Vec( 0*knobSpacing, 125 )), module, Ouros::RATE_KNOB)); + addInput(createInputCentered (knobStartPos.plus(Vec( 0*knobSpacing, 165 )), module, Ouros::RATE_INPUT)); + + addParam(createParamCentered(knobStartPos.plus(Vec( 1*knobSpacing, 110 )), module, Ouros::FEEDBACK_KNOB)); + addParam(createParamCentered (knobStartPos.plus(Vec( 1*knobSpacing, 140 )), module, Ouros::FEEDBACK_ATT_KNOB)); + addInput(createInputCentered (knobStartPos.plus(Vec( 1*knobSpacing, 165 )), module, Ouros::FEEDBACK_INPUT)); + + addParam(createParamCentered(knobStartPos.plus(Vec( 2*knobSpacing, 110 )), module, Ouros::EAT_KNOB)); + addParam(createParamCentered (knobStartPos.plus(Vec( 2*knobSpacing, 140 )), module, Ouros::EAT_ATT_KNOB)); + addInput(createInputCentered (knobStartPos.plus(Vec( 2*knobSpacing, 165 )), module, Ouros::EAT_INPUT)); + + addParam(createParamCentered(knobStartPos.plus(Vec( 3*knobSpacing, 110 )), module, Ouros::NODE_KNOB)); + addParam(createParamCentered (knobStartPos.plus(Vec( 3*knobSpacing, 140 )), module, Ouros::NODE_ATT_KNOB)); + addInput(createInputCentered (knobStartPos.plus(Vec( 3*knobSpacing, 165 )), module, Ouros::NODE_INPUT)); + + addOutput(createOutputCentered(knobStartPos.plus(Vec(3*knobSpacing, -102)), module, Ouros::L_OUTPUT)); + addOutput(createOutputCentered(knobStartPos.plus(Vec(3*knobSpacing, -72)), module, Ouros::R_OUTPUT)); + + // Create and add the PolarXYDisplay + PolarXYDisplay* polarDisplay = createWidget(Vec(56.5,55.5)); // Positioning + polarDisplay->box.size = Vec(50, 50); // Size of the display widget + polarDisplay->module = module; + addChild(polarDisplay); + + } +}; + +Model* modelOuros = createModel("Ouros"); \ No newline at end of file diff --git a/src/plugin.cpp b/src/plugin.cpp index 1d80b8c..f8ae9a9 100644 --- a/src/plugin.cpp +++ b/src/plugin.cpp @@ -18,6 +18,7 @@ void init(Plugin* p) { p->addModel(modelCollatz); p->addModel(modelStrings); p->addModel(modelMagnets); + p->addModel(modelOuros); // Any other plugin initialization may go here. diff --git a/src/plugin.hpp b/src/plugin.hpp index ebe4298..17c2a25 100644 --- a/src/plugin.hpp +++ b/src/plugin.hpp @@ -18,3 +18,4 @@ extern Model* modelHexMod; extern Model* modelCollatz; extern Model* modelStrings; extern Model* modelMagnets; +extern Model* modelOuros;