diff --git a/src/DisplayHandler.cpp b/src/DisplayHandler.cpp index 2cd0cf9..1486e70 100644 --- a/src/DisplayHandler.cpp +++ b/src/DisplayHandler.cpp @@ -397,7 +397,7 @@ void DisplayHandler::render() { } void DisplayHandler::renderMainPage() { - std::string bpm_string = "BPM: " + std::to_string(BPM); + std::string bpm_string = "BPM: " + std::to_string((uint8_t)BPM); if (cursorPosition == 0) { if (cursorClick == 1) { diff --git a/src/Gate.cpp b/src/Gate.cpp index 97f7720..a280dea 100644 --- a/src/Gate.cpp +++ b/src/Gate.cpp @@ -9,18 +9,17 @@ #include #include #include -#include #ifndef max -#define max(a,b) (((a) > (b)) ? (a) : (b)) +#define max(a, b) (((a) > (b)) ? (a) : (b)) #endif - -Gate::Gate(uint8_t pin, uint8_t idx, uint8_t slotIdx1, uint8_t slotIdx2) : Output(pin, idx, slotIdx1, slotIdx2) { +Gate::Gate(uint8_t pin, uint8_t idx, uint8_t slotIdx1, uint8_t slotIdx2) + : Output(pin, idx, slotIdx1, slotIdx2) { this->pin = pin; - this->idx = idx; - this->slotIdx1 = slotIdx1; - this->slotIdx2 = slotIdx2; + this->idx = idx; + this->slotIdx1 = slotIdx1; + this->slotIdx2 = slotIdx2; state = 0; @@ -37,48 +36,48 @@ Gate::Gate(uint8_t pin, uint8_t idx, uint8_t slotIdx1, uint8_t slotIdx2) : Outpu p = 100; // probability of a pulse level = 100; - modDest1 = (idx + 1) % 8; - modDest2 = (idx + 2) % 8; + modDest1 = (idx + 1) % 8; + modDest2 = (idx + 2) % 8; } - void Gate::pack(OutputConfig &cfg) { - cfg.type = TYPE_GATE; + cfg.type = TYPE_GATE; - GateSettings* s = (GateSettings*)cfg.data; + GateSettings *s = (GateSettings *)cfg.data; - s->modifierSelectionIndex = this->modifierSelectionIndex; - s->divideMode = this->divideMode; - s->modifier = this->modifier; - s->width = this->width; - s->p = this->p; - s->level = this->level; - s->shape = (uint8_t)this->shape; + s->modifierSelectionIndex = this->modifierSelectionIndex; + s->divideMode = this->divideMode; + s->modifier = this->modifier; + s->width = this->width; + s->p = this->p; + s->level = this->level; + s->shape = (uint8_t)this->shape; } void Gate::unpack(const OutputConfig &cfg) { - if (cfg.type != TYPE_GATE) return; + if (cfg.type != TYPE_GATE) + return; - GateSettings s; - memcpy(&s, cfg.data, sizeof(GateSettings)); + GateSettings s; + memcpy(&s, cfg.data, sizeof(GateSettings)); - this->modifierSelectionIndex = s.modifierSelectionIndex; - this->divideMode = s.divideMode; - this->modifier = s.modifier; - this->width = s.width; - this->p = s.p; - this->level = s.level; - this->shape = (WaveShape)s.shape; + this->modifierSelectionIndex = s.modifierSelectionIndex; + this->divideMode = s.divideMode; + this->modifier = s.modifier; + this->width = s.width; + this->p = s.p; + this->level = s.level; + this->shape = (WaveShape)s.shape; - setDiv(this->modifierSelectionIndex); - setWidth(this->width); + setDiv(this->modifierSelectionIndex); + setWidth(this->width); } void Gate::setupPatches() { - - matrix.patch(this->slotIdx1, this->modDest1, this->idx, DEST_LEVEL, 100, false); - - matrix.patch(this->slotIdx2, this->modDest2, this->idx, DEST_LEVEL, 100, false); + matrix.patch(this->slotIdx1, this->modDest1, this->idx, DEST_LEVEL, 100, + false); + matrix.patch(this->slotIdx2, this->modDest2, this->idx, DEST_LEVEL, 100, + false); } void Gate::setLen(uint32_t currentPeriod) { @@ -199,8 +198,6 @@ void Gate::setDiv(uint8_t modifier_selecton_index) { this->lastTriggerTick = 0xFFFFFFFF; setWidth(this->width); - // this is called in width, check if needed still? - calculatePulseWidth(); }; void Gate::setWidth(uint16_t newWidth) { @@ -239,22 +236,16 @@ void Gate::calculatePulseWidth() { return; } - // Cap the width at 99% so it doesn't bleed into the next trigger - float effectiveWidth = (float)this->width; - if (effectiveWidth > 99.0f) effectiveWidth = 99.0f; - - this->pulseWidthTicks = (uint32_t)((float)this->tickInterval * (effectiveWidth / 100.0f)); - - // Minimum 1 tick so we actually see a pulse if (this->width > 0 && this->pulseWidthTicks == 0) { this->pulseWidthTicks = 1; } } void Gate::turnOn() { - if (!isEnabled || tickInterval == 0) return; + if (!isEnabled || tickInterval == 0) { + return; + } - // Trigger on the interval, ensuring we don't double-trigger on the same tick if (MASTER_TICK % tickInterval == 0 && MASTER_TICK != lastTriggerTick) { lastTriggerTick = MASTER_TICK; @@ -267,46 +258,46 @@ void Gate::turnOn() { // Swing triggerCount++; - int32_t swingOffset = (int32_t)((float)tickInterval * ((float)swing - 50.0f) / 100.0f); - + int32_t swingOffset = + (int32_t)((float)tickInterval * ((float)swing - 50.0f) / 100.0f); + if (triggerCount % 2 == 0) { - // Use max(0, offset) to prevent early triggers from breaking the state machine scheduledTick = MASTER_TICK + (uint32_t)max(0, swingOffset); } else { scheduledTick = MASTER_TICK; } } - // Execution if (MASTER_TICK >= scheduledTick && !state) { state = 1; startTick = MASTER_TICK; - + calculatePulseWidth(); stopTick = startTick + pulseWidthTicks; - - scheduledTick = 0xFFFFFFFF; + + scheduledTick = 0xFFFFFFFF; currentRandomVal = (float)rand() / (float)RAND_MAX; } } void Gate::update() { - // 1. EXIT EARLY IF OFF if (!state && !sticky) { lastOutVal = 0.0f; writeAnalog(0); return; } - // 2. LIVE WIDTH MODULATION - float effectiveWidth = (float)width + (widthMod * 100.0f); - if (effectiveWidth > 100.0f) effectiveWidth = 100.0f; - if (effectiveWidth < 0.0f) effectiveWidth = 0.0f; + // width + float effectiveWidth = (float)width + (widthMod * 100.0f); + if (effectiveWidth > 100.0f) + effectiveWidth = 100.0f; + if (effectiveWidth < 0.0f) + effectiveWidth = 0.0f; - uint32_t modulatedTicks = (uint32_t)((float)this->tickInterval * (effectiveWidth / 100.0f)); + uint32_t modulatedTicks = + (uint32_t)((float)this->tickInterval * (effectiveWidth / 100.0f)); this->stopTick = startTick + modulatedTicks; - // 3. THE HARD SYNC // Only kill the gate if width is strictly less than 100% if (effectiveWidth < 100.0f) { if (MASTER_TICK >= stopTick) { @@ -319,63 +310,103 @@ void Gate::update() { } } - // 4. HYBRID SMOOTHNESS MATH uint64_t now = time_us_64(); - uint64_t usSinceLastTick = (now > last_clk_us) ? (now - last_clk_us) : 0; + uint64_t usSinceLastTick = (now > last_clk_us) ? (now - last_clk_us) : 0; double current_BPM_for_math = (double)filteredBPM; - if (current_BPM_for_math < 1.0) current_BPM_for_math = 1.0; + if (current_BPM_for_math < 1.0) + current_BPM_for_math = 1.0; double us_per_tick = 60000000.0 / (current_BPM_for_math * (double)PPQN); - - float subTick = (float)usSinceLastTick / (float)us_per_tick; - if (subTick > 0.99f) subTick = 0.99f; - // --- THE SINE WAVE FIX --- - // If width is 100%, we calculate phase based on the WHOLE interval. - // If width < 100%, we calculate phase based on the PULSE duration. + float subTick = (float)usSinceLastTick / (float)us_per_tick; + if (subTick > 0.99f) + subTick = 0.99f; + + // If width is 100%, calculate phase based on the WHOLE interval. + // If width < 100%, calculate phase based on the PULSE duration. float elapsedTicks = (float)(MASTER_TICK - startTick) + subTick; - float totalDurationTicks = (effectiveWidth >= 100.0f) ? (float)tickInterval : (float)(stopTick - startTick); - - if (totalDurationTicks < 1.0f) totalDurationTicks = 1.0f; + float totalDurationTicks = (effectiveWidth >= 100.0f) + ? (float)tickInterval + : (float)(stopTick - startTick); + + if (totalDurationTicks < 1.0f) + totalDurationTicks = 1.0f; float phase = elapsedTicks / totalDurationTicks; - // Keep phase looping if we are at 100% width (so LFOs/Sines keep moving) + // Keep phase looping if at 100% width if (effectiveWidth >= 100.0f) { - while (phase >= 1.0f) phase -= 1.0f; + while (phase >= 1.0f) + phase -= 1.0f; } else { - if (phase > 1.0f) phase = 1.0f; + if (phase > 1.0f) + phase = 1.0f; } - if (phase < 0.0f) phase = 0.0f; - // 5. WAVEFORM GENERATION + if (phase < 0.0f) + phase = 0.0f; + float outVal = 0; switch (shape) { - case SINE: outVal = (sinf(phase * 2.0f * 3.14159265f) * 0.5f) + 0.5f; break; - case HALFSINE: outVal = sinf(phase * 3.14159265f); break; - case TRIANGLE: outVal = (phase < 0.5f) ? (phase * 2.0f) : (2.0f - (phase * 2.0f)); break; - case SAW: outVal = 1.0f - phase; break; - case RAMP: outVal = phase; break; - case EXP: outVal = expf(-5.0f * phase); break; - case REXP: outVal = expf(5.0f * (phase - 1.0f)); break; - case LOG: outVal = 1.0f - expf(-5.0f * phase); break; - case SQUARE: outVal = 1.0f; break; - case BOUNCE: outVal = fabsf(sinf(phase * 3.14159265f * 2.0f)); break; - case SIGMO: outVal = phase * phase * (3.0f - 2.0f * phase); break; - case WOBBLE: outVal = expf(-3.0f * phase) * cosf(phase * 3.14159265f * 4.0f); - if (outVal < 0) outVal = 0; break; - case STEPDW: outVal = 1.0f - (floorf(phase * 4.0f) / 3.0f); break; - case STEPUP: outVal = floorf(phase * 4.0f) / 3.0f; break; - case SH: outVal = currentRandomVal; break; - default: outVal = 1.0f; break; + case SINE: + outVal = (sinf(phase * 2.0f * 3.14159265f) * 0.5f) + 0.5f; + break; + case HALFSINE: + outVal = sinf(phase * 3.14159265f); + break; + case TRIANGLE: + outVal = (phase < 0.5f) ? (phase * 2.0f) : (2.0f - (phase * 2.0f)); + break; + case SAW: + outVal = 1.0f - phase; + break; + case RAMP: + outVal = phase; + break; + case EXP: + outVal = expf(-5.0f * phase); + break; + case REXP: + outVal = expf(5.0f * (phase - 1.0f)); + break; + case LOG: + outVal = 1.0f - expf(-5.0f * phase); + break; + case SQUARE: + outVal = 1.0f; + break; + case BOUNCE: + outVal = fabsf(sinf(phase * 3.14159265f * 2.0f)); + break; + case SIGMO: + outVal = phase * phase * (3.0f - 2.0f * phase); + break; + case WOBBLE: + outVal = expf(-3.0f * phase) * cosf(phase * 3.14159265f * 4.0f); + if (outVal < 0) + outVal = 0; + break; + case STEPDW: + outVal = 1.0f - (floorf(phase * 4.0f) / 3.0f); + break; + case STEPUP: + outVal = floorf(phase * 4.0f) / 3.0f; + break; + case SH: + outVal = currentRandomVal; + break; + default: + outVal = 1.0f; + break; } this->lastOutVal = outVal; - // 6. FINAL LEVEL & MODULATION - float finalLevel = ((float)this->level / 100.0f) + (this->levelMod); - if (finalLevel > 1.0f) finalLevel = 1.0f; - if (finalLevel < 0.0f) finalLevel = 0.0f; + float finalLevel = ((float)this->level / 100.0f) + (this->levelMod); + if (finalLevel > 1.0f) + finalLevel = 1.0f; + if (finalLevel < 0.0f) + finalLevel = 0.0f; writeAnalog((uint16_t)(outVal * 1023.0f * finalLevel)); } diff --git a/src/main.cpp b/src/main.cpp index f93b8ca..990ff66 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -33,13 +33,13 @@ volatile uint8_t EXTPPQNIdx = 0; volatile uint64_t last_clk_us = 0; volatile uint64_t last_valid_clk_us; volatile bool EXTERNAL_CLOCK = false; -volatile float filteredBPM = 60.0f; // High-precision BPM for smooth LFOs -volatile uint64_t last_external_pulse_us = - 0; // Tracks the last Arturia pulse for the watchdog +volatile float filteredBPM = 60.0f; +volatile uint64_t last_external_pulse_us = 0; const uint64_t CLOCK_TIMEOUT_US = 2000000; uint64_t pulse_intervals[AVG_SAMPLES] = {0}; uint8_t pulse_idx = 0; uint64_t last_external_clk_us = 0; +uint8_t BPM_UI_REFRESH = 0; bool external_pulse_received = false; ModMatrix matrix; @@ -91,10 +91,6 @@ void update_BPM(bool up) { update_period(); - // for (auto g : outputs) { - // g->setWidth(g->width); - // } - if (!EXTERNAL_CLOCK) { init_timer(period_us); } else { @@ -155,13 +151,12 @@ void handle_outs() { } void gpio_callback(uint gpio, uint32_t events) { -if (gpio == IN_CLK_PIN && (events & GPIO_IRQ_EDGE_RISE)) { + if (gpio == IN_CLK_PIN && (events & GPIO_IRQ_EDGE_RISE)) { uint64_t now = to_us_since_boot(get_absolute_time()); - - // 1. Debounce - if (now - last_valid_clk_us < 1000) return; - // 2. BPM Math - Use 'last_external_clk_us' specifically + if (now - last_valid_clk_us < 1000) + return; + if (last_external_clk_us > 0) { uint64_t latest_diff = now - last_external_clk_us; @@ -177,23 +172,27 @@ if (gpio == IN_CLK_PIN && (events & GPIO_IRQ_EDGE_RISE)) { } } - if (count > 0) { + if (count > 0) { double avg_diff = (double)sum / (double)count; uint16_t incomingPPQN = PPQNOPTS[EXTPPQNIdx]; - - // Use 60,000,000.0 (double) to ensure high precision + double calculatedBPM = 60000000.0 / (avg_diff * (double)incomingPPQN); if (calculatedBPM > 20.0 && calculatedBPM < 300.0) { - // Slow down the LPF even more (0.05 instead of 0.1) - // This makes the screen feel "heavy" and professional like a real synth - filteredBPM = filteredBPM + (0.05f * ((float)calculatedBPM - filteredBPM)); - } - }} + float diff = (float)calculatedBPM - filteredBPM; + + if (fabsf(diff) > 5.0f || filteredBPM < 1.0f) { + filteredBPM = (float)calculatedBPM; + } else { + filteredBPM += (0.3f * diff); + } + } + } + } - // 3. Sync Logic uint32_t ticks_per_pulse = 96 / PPQNOPTS[EXTPPQNIdx]; - MASTER_TICK = ((MASTER_TICK + (ticks_per_pulse / 2)) / ticks_per_pulse) * ticks_per_pulse; + MASTER_TICK = ((MASTER_TICK + (ticks_per_pulse / 2)) / ticks_per_pulse) * + ticks_per_pulse; for (int i = 0; i < 8; i++) { if (outputs[i]->lastTriggerTick > MASTER_TICK) { @@ -201,17 +200,29 @@ if (gpio == IN_CLK_PIN && (events & GPIO_IRQ_EDGE_RISE)) { } } - // 4. Update Timestamps - last_external_clk_us = now; // Store for next external pulse - last_clk_us = now; // Update for the Gate's sub-tick interpolation + last_external_clk_us = now; + last_clk_us = now; last_valid_clk_us = now; last_external_pulse_us = now; external_pulse_received = true; EXTERNAL_CLOCK = true; + + BPM_UI_REFRESH += 1; + if (BPM_UI_REFRESH % 4 == 0) { + display_handler.updateScreen = 1; + } } + if (gpio == IN_RUN_PIN) { + if (RUN) { + if (events & GPIO_IRQ_EDGE_RISE) { + PLAY = true; + } else if (events & GPIO_IRQ_EDGE_FALL) { + PLAY = false; + } + } + } - // SWITCH if (gpio == ENCODER_SW_PIN) { uint64_t now = to_us_since_boot(get_absolute_time()); static uint64_t last_sw_time = 0; @@ -244,10 +255,8 @@ void setup_ins() { adc_gpio_init(27); } -// Helper to scale your current range to 0.0 - 1.0 float fmap(float x, float in_min, float in_max) { float result = (x - in_min) / (in_max - in_min); - // Constraints to keep it between 0.0 and 1.0 if (result < 0.0f) return 0.0f; if (result > 1.0f) @@ -259,33 +268,24 @@ void update_cv() { static uint64_t last_adc_read = 0; uint64_t now = to_us_since_boot(get_absolute_time()); if (now - last_adc_read < 2000) - return; // 2ms is plenty fast + return; last_adc_read = now; - // Calibration (Adjust these based on your earlier -0.19 to 0.15 range) const float raw_min = -0.19f; const float raw_max = 0.15f; - const float offset_zero = 0.404f; // Your calibrated offset + const float offset_zero = 0.404f; for (int i = 0; i < 2; i++) { adc_select_input(i); - // CROSSTALK FIX: Dummy read to clear the ADC capacitor adc_read(); - busy_wait_us(10); // Tiny pause to settle + busy_wait_us(10); - // Actual read float raw_val = (float)adc_read() * (1.0f / 4095.0f); float centered = offset_zero - raw_val; - // SCALING & FLIPPING: - // By using (max - centered), we flip the inversion. float scaled = (centered - raw_min) / (raw_max - raw_min); - // Optional: If it's STILL upside down, use this instead: - // float scaled = 1.0f - ((centered - raw_min) / (raw_max - raw_min)); - - // Constrain 0.0 to 1.0 if (scaled < 0.01f) scaled = 0.0f; if (scaled > 1.0f) @@ -337,7 +337,6 @@ int main() { while (true) { uint64_t now = to_us_since_boot(get_absolute_time()); - // 1. WATCHDOG: Return to internal clock if pulses stop if (EXTERNAL_CLOCK && (now - last_external_pulse_us > CLOCK_TIMEOUT_US)) { EXTERNAL_CLOCK = false; BPM = globalSettings.bpm; @@ -346,7 +345,6 @@ int main() { printf("Clock Lost. Internal BPM Resumed.\n"); } - // 2. EXTERNAL PULSE UI UPDATES if (external_pulse_received) { external_pulse_received = false; @@ -360,8 +358,6 @@ int main() { last_ppqn_idx = EXTPPQNIdx; } - // This is purely for the screen/UI. - // The actual timing is being adjusted inside gpio_callback. BPM = (uint8_t)(filteredBPM + 0.5f); if (PLAY) { @@ -371,12 +367,11 @@ int main() { } } - // 3. REGULAR TASKS update_cv(); encoder_handler.update(); if (PLAY) { - handle_outs(); // This runs at the frequency set by our PLL + handle_outs(); } else { for (Gate *g : outputs) { g->turnOff();