From b7c5c27328c5e671f38ffd44c202c0027940108b Mon Sep 17 00:00:00 2001 From: Dominic DiTaranto Date: Thu, 5 Mar 2026 10:33:09 -0500 Subject: [PATCH] shapes, smoothing pwm, etc --- CMakeLists.txt | 1 + include/DisplayHandler.h | 2 +- include/Gate.h | 18 +- include/globals.h | 23 +++ master_clock.drawio | 153 ++++++++++++++ src/DisplayHandler.cpp | 80 +++++++- src/Gate.cpp | 419 ++++++++++++++++++++++++--------------- src/main.cpp | 17 +- 8 files changed, 544 insertions(+), 169 deletions(-) create mode 100644 master_clock.drawio diff --git a/CMakeLists.txt b/CMakeLists.txt index 8e28f11..f3e4941 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,7 @@ pico_generate_pio_header(clock ${CMAKE_CURRENT_LIST_DIR}/quadrature_encoder.pio) # Pull in standard library and hardware abstraction target_link_libraries(clock pico_stdlib + hardware_pwm hardware_gpio hardware_pio hardware_i2c diff --git a/include/DisplayHandler.h b/include/DisplayHandler.h index 78f3296..a978ebc 100644 --- a/include/DisplayHandler.h +++ b/include/DisplayHandler.h @@ -13,7 +13,7 @@ class DisplayHandler { char buffer[32]; uint8_t currentScreen; std::string screens[7]; - std::array out_pages = {"Exit", "Mod", "Width", "Prob", "Mute"}; + std::array out_pages = {"Exit", "Mod", "Shape", "Level", "Width", "Swing", "Prob", "Sticky", "Mute"}; bool onOutScreen = 0; void renderMainPage(); void renderOutPage(); diff --git a/include/Gate.h b/include/Gate.h index 2cd4b2d..cddfccb 100644 --- a/include/Gate.h +++ b/include/Gate.h @@ -4,27 +4,43 @@ #include #include "Output.h" +#include "globals.h" + class Gate : public Output { private: uint32_t dur; + uint32_t triggerCount; + uint32_t scheduledTick; + float currentRandomVal; uint32_t len; uint32_t lastTriggerTick = 0xFFFFFFFF; + uint64_t startTimeUs; + uint32_t pulseDurationUs; public: Gate(uint8_t pin); - + + WaveShape shape = SQUARE; + uint32_t startTick = 0; + uint32_t pulseWidthTicks = 0; + bool sticky = false; int8_t modifierSelectionIndex; uint8_t divideMode; + uint8_t swing = 50; uint16_t modifier; uint16_t tickInterval; + uint8_t level; uint8_t width; uint8_t p; void turnOn() override; + void update(); void turnOff() override; + void calculatePulseWidth(); + void writeAnalog(uint16_t val); void setLen(uint32_t currentPeriod); void setDiv(uint8_t modifier_selection_index); void setWidth(uint16_t newWidth); diff --git a/include/globals.h b/include/globals.h index 4343ba6..9ae888f 100644 --- a/include/globals.h +++ b/include/globals.h @@ -35,6 +35,29 @@ static constexpr uint8_t PPQN = 96; extern volatile uint32_t MASTER_TICK; +enum WaveShape { SQUARE, TRIANGLE, SAW, RAMP, EXP, HALFSINE, REXP, LOG, SINE, BOUNCE, SIGMO, WOBBLE, STEPDW, STEPUP, SH, SHAPE_COUNT}; + +inline const char* waveShapeToString(WaveShape shape) { + switch (shape) { + case SQUARE: return "SQR"; + case SINE: return "SINE"; + case TRIANGLE: return "TRI"; + case SAW: return "SAW"; + case EXP: return "EXP"; + case REXP: return "REXP"; + case RAMP: return "RAMP"; + case LOG: return "LOG"; + case BOUNCE: return "BNC"; + case WOBBLE: return "WOB"; + case SIGMO: return "SIG"; + case STEPDW: return "STPD"; + case STEPUP: return "STPU"; + case SH: return "S&H"; + case HALFSINE: return "HUMP"; + default: return "?"; + } +} + // Modifiers in UI order static std::array MODIFIERS = {32, 16, 12, 8, 6, 4, 3, 2, 0, 1, 2, 3, 4, 6, 8, 16, 32}; // Modifier type; 0 = multiplicaton, 1 = division; matched with MODIFIERS diff --git a/master_clock.drawio b/master_clock.drawio new file mode 100644 index 0000000..e33d8be --- /dev/null +++ b/master_clock.drawio @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DisplayHandler.cpp b/src/DisplayHandler.cpp index b61e72a..1b851b5 100644 --- a/src/DisplayHandler.cpp +++ b/src/DisplayHandler.cpp @@ -78,7 +78,43 @@ void DisplayHandler::moveCursor(bool dir) { outputs[currentOut]->modifierSelectionIndex = std::size(MOD_TYPES) - 1; } - } else if (currentScreen == 2) { // width control + } else if (currentScreen == 2) { // shape control + int currentShape = (int)outputs[currentOut]->shape; + + if (dir == 1) { + currentShape++; + } else { + currentShape--; + } + + if (currentShape >= SHAPE_COUNT) { + currentShape = 0; + } + + if (currentShape < 0) { + currentShape = SHAPE_COUNT - 1; + } + + outputs[currentOut]->shape = (WaveShape)currentShape; + outputs[currentOut]->writeAnalog(0); + + } else if (currentScreen == 3) { // level control + outputs[currentOut]->editing = 1; + if (dir == 1) { + outputs[currentOut]->level++; + } else { + outputs[currentOut]->level--; + } + + if (outputs[currentOut]->level > 100) { + outputs[currentOut]->level = 100; + } + + if (outputs[currentOut]->level < 1) { + outputs[currentOut]->level = 1; + } + + } else if (currentScreen == 4) { // width control outputs[currentOut]->editing = 1; if (dir == 1) { outputs[currentOut]->width++; @@ -96,7 +132,23 @@ void DisplayHandler::moveCursor(bool dir) { outputs[currentOut]->setWidth(outputs[currentOut]->width); - } else if (currentScreen == 3) { // PROBABILITY + } else if (currentScreen == 5) { // SWING + + outputs[currentOut]->editing = 1; + if (dir == 1) { + outputs[currentOut]->swing++; + } else { + outputs[currentOut]->swing--; + } + + if (outputs[currentOut]->swing > 100) { + outputs[currentOut]->swing = 100; + } + + if (outputs[currentOut]->swing < 50) { + outputs[currentOut]->swing = 50; + } + } else if (currentScreen == 6) { // PROBABILITY outputs[currentOut]->editing = 1; if (dir == 1) { @@ -112,7 +164,10 @@ void DisplayHandler::moveCursor(bool dir) { if (outputs[currentOut]->p < 0) { outputs[currentOut]->p = 0; } - } else if (currentScreen == 4) { // MUTE + } else if (currentScreen == 7) { // STICKY + outputs[currentOut]->sticky ^= true; + + } else if (currentScreen == 8) { // MUTE outputs[currentOut]->editing = 1; outputs[currentOut]->isEnabled ^= true; @@ -267,12 +322,25 @@ void DisplayHandler::renderOutPage() { uint8_t modifier_selection_index = outputs[currentOut]->modifierSelectionIndex; param_string = MODIFIER_STRINGS[modifier_selection_index]; - } else if (currentScreen == 2) { // Width screen + } else if (currentScreen == 2) { // shape screen + param_string = waveShapeToString(outputs[currentOut]->shape); + + } else if (currentScreen == 3) { // level screen + param_string = std::to_string(outputs[currentOut]->level) + "%"; + + } else if (currentScreen == 4) { // Width screen param_string = std::to_string(outputs[currentOut]->width) + "%"; - } else if (currentScreen == 3) { // Probability screen + } else if (currentScreen == 5) { // Swing screen + param_string = std::to_string(outputs[currentOut]->swing) + "%"; + + } else if (currentScreen == 6) { // Probability screen param_string = std::to_string(outputs[currentOut]->p) + "%"; - } else if (currentScreen == 4) { // Mute Screen + + } else if (currentScreen == 7) { // STICKY Screen + param_string = outputs[currentOut]->sticky ? "ON" : "OFF"; + + } else if (currentScreen == 8) { // Mute Screen param_string = outputs[currentOut]->isEnabled ? "ON" : "OFF"; } diff --git a/src/Gate.cpp b/src/Gate.cpp index 4dba464..5e45451 100644 --- a/src/Gate.cpp +++ b/src/Gate.cpp @@ -1,196 +1,297 @@ // Gate.cpp #include "Gate.h" #include "globals.h" +#include "hardware/pwm.h" #include +#include - -Gate::Gate(uint8_t pin) : Output(pin){ +Gate::Gate(uint8_t pin) : Output(pin) { this->pin = pin; state = 0; - editing = 0; + editing = 0; - modifierSelectionIndex = 8; - divideMode = 0; // 1 divison | 0 multiplication - modifier = 0; // divide mode modifier (4x, /32, etc) + modifierSelectionIndex = 8; + divideMode = 0; // 1 divison | 0 multiplication + modifier = 0; // divide mode modifier (4x, /32, etc) - dur = 0; // how long pulse is on - width = 50; // pulse width - len = 0; // max len a pulse can be on, as determined by width + dur = 0; // how long pulse is on + width = 50; // pulse width + len = 0; // max len a pulse can be on, as determined by width - p = 100; // probability of a pulse + p = 100; // probability of a pulse + level = 100; } - void Gate::setLen(uint32_t currentPeriod) { - len = (uint32_t)((double)currentPeriod * (width / 100.0) / 1000.0); + len = (uint32_t)((double)currentPeriod * (width / 100.0) / 1000.0); } - void Gate::setDiv(uint8_t modifier_selecton_index) { - switch(modifier_selecton_index) { - case 0: // x32 - tickInterval = 3; - isEnabled = true; - divideMode = 0; - modifier = 32; - break; - case 1: // x16 - tickInterval = 6; - isEnabled = true; - divideMode = 0; - modifier = 16; - break; - case 2: // x12 - tickInterval = 8; - isEnabled = true; - divideMode = 0; - modifier = 12; - break; - case 3: // x8 - tickInterval = 12; - isEnabled = true; - divideMode = 0; - modifier = 8; - break; - case 4: // x6 - tickInterval = 16; - isEnabled = true; - divideMode = 0; - modifier = 6; - break; - case 5: // x4 - tickInterval = 24; - isEnabled = true; - divideMode = 0; - modifier = 4; - break; - case 6: // x3 - tickInterval = 32; - isEnabled = true; - divideMode = 0; - modifier = 3; - break; - case 7: // x2 - tickInterval = 48; - isEnabled = true; - divideMode = 0; - modifier = 2; - break; - case 8: // 0 (OFF) - tickInterval = 0; - isEnabled = false; - divideMode = 0; - modifier = 0; - break; - case 9: // /1 - tickInterval = 96; - isEnabled = true; - divideMode = 1; - modifier = 1; - break; - case 10: // /2 - tickInterval = 192; - isEnabled = true; - divideMode = 1; - modifier = 2; - break; - case 11: // /3 - tickInterval = 288; - isEnabled = true; - divideMode = 1; - modifier = 3; - break; - case 12: // /4 - tickInterval = 384; - isEnabled = true; - divideMode = 1; - modifier = 4; - break; - case 13: // /6 - tickInterval = 576; - isEnabled = true; - divideMode = 1; - modifier = 4; - break; - case 14: // /8 - tickInterval = 768; - isEnabled = true; - divideMode = 1; - modifier = 8; - break; - case 15: // /16 - tickInterval = 1536; - isEnabled = true; - divideMode = 1; - modifier = 16; - break; - case 16: // /32 - tickInterval = 3072; - isEnabled = true; - divideMode = 1; - modifier = 16; - break; - default: - tickInterval = 96; - isEnabled = true; - divideMode = 1; - } + switch (modifier_selecton_index) { + case 0: // x32 + tickInterval = 3; + isEnabled = true; + divideMode = 0; + modifier = 32; + break; + case 1: // x16 + tickInterval = 6; + isEnabled = true; + divideMode = 0; + modifier = 16; + break; + case 2: // x12 + tickInterval = 8; + isEnabled = true; + divideMode = 0; + modifier = 12; + break; + case 3: // x8 + tickInterval = 12; + isEnabled = true; + divideMode = 0; + modifier = 8; + break; + case 4: // x6 + tickInterval = 16; + isEnabled = true; + divideMode = 0; + modifier = 6; + break; + case 5: // x4 + tickInterval = 24; + isEnabled = true; + divideMode = 0; + modifier = 4; + break; + case 6: // x3 + tickInterval = 32; + isEnabled = true; + divideMode = 0; + modifier = 3; + break; + case 7: // x2 + tickInterval = 48; + isEnabled = true; + divideMode = 0; + modifier = 2; + break; + case 8: // 0 (OFF) + tickInterval = 0; + isEnabled = false; + divideMode = 0; + modifier = 0; + break; + case 9: // /1 + tickInterval = 96; + isEnabled = true; + divideMode = 1; + modifier = 1; + break; + case 10: // /2 + tickInterval = 192; + isEnabled = true; + divideMode = 1; + modifier = 2; + break; + case 11: // /3 + tickInterval = 288; + isEnabled = true; + divideMode = 1; + modifier = 3; + break; + case 12: // /4 + tickInterval = 384; + isEnabled = true; + divideMode = 1; + modifier = 4; + break; + case 13: // /6 + tickInterval = 576; + isEnabled = true; + divideMode = 1; + modifier = 4; + break; + case 14: // /8 + tickInterval = 768; + isEnabled = true; + divideMode = 1; + modifier = 8; + break; + case 15: // /16 + tickInterval = 1536; + isEnabled = true; + divideMode = 1; + modifier = 16; + break; + case 16: // /32 + tickInterval = 3072; + isEnabled = true; + divideMode = 1; + modifier = 16; + break; + default: + tickInterval = 96; + isEnabled = true; + divideMode = 1; + } - // TODO: check if this is actually needed - setWidth(this->width); + setWidth(this->width); + // this is called in width, check if needed still? + calculatePulseWidth(); }; - void Gate::setWidth(uint16_t newWidth) { - this->width = newWidth; - - double ms_per_tick = (60000.0 / (double)BPM) / 96.0; + this->width = newWidth; - double ms_between_triggers = ms_per_tick * (double)this->tickInterval; + double ms_per_tick = (60000.0 / (double)BPM) / 96.0; - this->len = (uint32_t)(ms_between_triggers * (this->width / 100.0)); + double ms_between_triggers = ms_per_tick * (double)this->tickInterval; - uint32_t max_allowed_len = (ms_between_triggers > 5) ? (uint32_t)ms_between_triggers - 2 : 1; - - if (this->len > max_allowed_len) { - this->len = max_allowed_len; - } + this->len = (uint32_t)(ms_between_triggers * (this->width / 100.0)); - if (this->len < 1) this->len = 1; + uint32_t max_allowed_len = + (ms_between_triggers > 5) ? (uint32_t)ms_between_triggers - 2 : 1; + + if (this->len > max_allowed_len) { + this->len = max_allowed_len; + } + + if (this->len < 1) + this->len = 1; + + double us_per_tick = 625000.0 / (double)BPM; + + calculatePulseWidth(); + + this->pulseDurationUs = + (uint32_t)(us_per_tick * (double)this->pulseWidthTicks); + + if (this->pulseDurationUs < 100) + this->pulseDurationUs = 100; } +void Gate::calculatePulseWidth() { + if (tickInterval == 0 || !isEnabled) { + pulseWidthTicks = 0; + return; + } + // If tickInterval is 96 and width is 50, pulseWidthTicks becomes 48 + this->pulseWidthTicks = + (uint32_t)((float)this->tickInterval * (this->width / 100.0f)); + + // Safety: ensure a pulse is at least 1 tick long if width > 0 + if (this->width > 0 && this->pulseWidthTicks == 0) { + this->pulseWidthTicks = 1; + } +} void Gate::turnOn() { - if (!isEnabled || tickInterval == 0) return; + if (!isEnabled || tickInterval == 0) + return; - if (MASTER_TICK % tickInterval == 0) { - if (MASTER_TICK != lastTriggerTick) { - lastTriggerTick = MASTER_TICK; + if (MASTER_TICK % tickInterval == 0) { + if (MASTER_TICK != lastTriggerTick) { + lastTriggerTick = MASTER_TICK; - uint8_t pRes = 1; - if (p < 100) { - if ((rand() % 100) + 1 > p) pRes = 0; - } + if (p < 100 && (rand() % 100) + 1 > p) { + scheduledTick = 0xFFFFFFFF; // ignore interval + return; + } - if (pRes == 1) { - state = 1; - gpio_put(pin, 1); - dur = millis(); - } - } - } - else { - lastTriggerTick = 0xFFFFFFFF; + // swing + triggerCount++; + + uint32_t swingDelayTicks = + (uint32_t)((float)tickInterval * ((float)swing - 50.0f) / 100.0f); + + if (triggerCount % 2 == 0) { + scheduledTick = MASTER_TICK + swingDelayTicks; + } else { + scheduledTick = MASTER_TICK; + } } + } + + if (MASTER_TICK == scheduledTick && !state) { + state = 1; + startTick = MASTER_TICK; + startTimeUs = time_us_64(); + currentRandomVal = (float)rand() / (float)RAND_MAX; + } } +void Gate::update() { + if (!state && !sticky) + return; -void Gate::turnOff() { - if (state == 1) { - if (millis() - dur >= len) { - state = 0; - gpio_put(pin, 0); - } - } + uint64_t now = time_us_64(); + uint32_t elapsedUs = (uint32_t)(now - startTimeUs); + + if (elapsedUs >= pulseDurationUs) { + state = 0; + if (!sticky) + writeAnalog(0); + return; + } + + float phase = (float)elapsedUs / (float)pulseDurationUs; + float outVal = 0; + + switch (shape) { + case SINE: + outVal = (sinf(phase * 2.0f * 3.14159f) * 0.5f) + 0.5f; + break; + case HALFSINE: // AKA HUMP + outVal = sinf(phase * 3.14159f); + 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.14159f * 2.0f)); + break; + case SIGMO: + outVal = phase * phase * (3.0f - 2.0f * phase); + break; + case WOBBLE: + outVal = expf(-3.0f * phase) * cosf(phase * 3.14159f * 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; + } + + writeAnalog((outVal * 1023.0f) * ((float)level / 100)); } + +void Gate::writeAnalog(uint16_t val) { pwm_set_gpio_level(pin, val); } + +void Gate::turnOff() { writeAnalog(0); } diff --git a/src/main.cpp b/src/main.cpp index c155442..1a9d348 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,6 +10,7 @@ #include "Gate.h" #include "DisplayHandler.h" #include "EncoderHandler.h" +#include "hardware/pwm.h" // Time based operations @@ -38,7 +39,6 @@ static DisplayHandler display_handler(outputs); static EncoderHandler encoder_handler(&display_handler); - bool timer_callback(struct repeating_timer *t) { if (PLAY == 1) { MASTER_TICK += 1; @@ -67,6 +67,11 @@ void update_BPM(bool up) { } update_period(); + + + for (auto g : outputs) { + g->setWidth(g->width); + } } @@ -86,6 +91,14 @@ void setup_outs() { for (auto g : outputs) { gpio_init(g->pin); gpio_set_dir(g->pin, GPIO_OUT); + + gpio_set_function(g->pin, GPIO_FUNC_PWM); + uint slice_num = pwm_gpio_to_slice_num(g->pin); + pwm_set_wrap(slice_num, 1023); + pwm_set_clkdiv(slice_num, 1.0f); + + pwm_set_gpio_level(g->pin, 0); + pwm_set_enabled(slice_num, true); g->setLen(period_us); } @@ -98,7 +111,7 @@ void setup_outs() { void handle_outs() { for (Gate* g: outputs) { g->turnOn(); - g->turnOff(); + g->update(); } }