diff --git a/Firmware/main/CMakeLists.txt b/Firmware/main/CMakeLists.txt index 240c109..5e7b90f 100644 --- a/Firmware/main/CMakeLists.txt +++ b/Firmware/main/CMakeLists.txt @@ -9,5 +9,6 @@ idf_component_register(SRCS src/shutdowner.cpp src/buttons.cpp src/power_helper.cpp - PRIV_REQUIRES spi_flash esp_driver_i2c driver sdk-esp esp_timer + src/buzzer.cpp + PRIV_REQUIRES spi_flash esp_driver_i2c driver sdk-esp esp_timer nvs_flash INCLUDE_DIRS "include") diff --git a/Firmware/main/include/buzzer.hpp b/Firmware/main/include/buzzer.hpp new file mode 100644 index 0000000..941688c --- /dev/null +++ b/Firmware/main/include/buzzer.hpp @@ -0,0 +1,54 @@ +// Simple piezo buzzer helper using LEDC (PWM) for square wave tones. +// Provides a tiny queued pattern player for short game SFX without blocking. +#pragma once + +#include + +class Buzzer { +public: + static Buzzer &get(); + + void init(); // call once from app_main + + // Queue a tone. freq=0 => silence. gap_ms is silence after tone before next. + void tone(uint32_t freq, uint32_t duration_ms, uint32_t gap_ms = 0); + + // Convenience SFX + void beepRotate(); + void beepMove(); + void beepLock(); + void beepLines(int lines); // 1..4 lines + void beepLevelUp(int level); // after increment + void beepGameOver(); + + // Mute controls + void setMuted(bool m); + void toggleMuted(); + bool isMuted() const { return _muted; } + + // Persistence + void loadState(); + void saveState(); + +private: + struct Step { uint32_t freq; uint32_t dur_ms; uint32_t gap_ms; }; + static constexpr int MAX_QUEUE = 16; + Step _queue[MAX_QUEUE]{}; + int _q_head = 0; // inclusive + int _q_tail = 0; // exclusive + bool _running = false; + bool _in_gap = false; + void *_timer = nullptr; // esp_timer_handle_t (opaque here) + bool _muted = false; + + Buzzer() = default; + void enqueue(const Step &s); + bool empty() const { return _q_head == _q_tail; } + Step &front() { return _queue[_q_head]; } + void popFront(); + void startNext(); + void schedule(uint32_t ms, bool gapPhase); + void applyFreq(uint32_t freq); + static void timerCb(void *arg); + void clearQueue() { _q_head = _q_tail = 0; } +}; diff --git a/Firmware/main/include/config.hpp b/Firmware/main/include/config.hpp index abd7bde..97be0a9 100644 --- a/Firmware/main/include/config.hpp +++ b/Firmware/main/include/config.hpp @@ -18,6 +18,8 @@ #define DISP_WIDTH 400 #define DISP_HEIGHT 240 +#define BUZZER_PIN GPIO_NUM_25 + #define PWR_INT GPIO_NUM_10 #define PWR_KILL GPIO_NUM_12 diff --git a/Firmware/main/src/app_main.cpp b/Firmware/main/src/app_main.cpp index 425f6c2..69a8e65 100644 --- a/Firmware/main/src/app_main.cpp +++ b/Firmware/main/src/app_main.cpp @@ -39,6 +39,7 @@ // Battery monitor header (conditionally included) #include #include "power_helper.hpp" +#include namespace cfg { constexpr int BoardW = 10; @@ -75,7 +76,7 @@ struct IFramebuffer { }; struct InputState { - bool left = false, right = false, down = false, rotate = false, back = false; + bool left = false, right = false, down = false, rotate = false, back = false, select = false; }; struct IInput { virtual ~IInput() = default; @@ -123,6 +124,8 @@ struct PlatformInput final : IInput { s.rotate = true; // rotate if (p & BTN_B) s.back = true; // pause/back + if (p & BTN_SELECT) + s.select = true; // mute toggle return s; } }; @@ -829,6 +832,12 @@ private: drawText(x, y, line1); drawText(x, y + 10, line2); drawText(x, y + 20, line3); + if (Buzzer::get().isMuted()) { + // Place MUTED at top-right, 5 chars * 6px = 30px width + int mx = fb.width() - 30 - 4; + int my = 4; + drawText(mx, my, "MUTED"); + } } }; @@ -973,6 +982,11 @@ public: dirty = true; } backPrev = st.back; + // Mute toggle (Select button) + if (st.select && !selectPrev) { + Buzzer::get().toggleMuted(); + } + selectPrev = st.select; if (paused) { uint64_t logicEndUs = esp_timer_get_time(); { @@ -1005,8 +1019,10 @@ public: } // Rotation if (st.rotate && !rotPrev) { - if (tryRotate(+1)) + if (tryRotate(+1)) { dirty = true; + Buzzer::get().beepRotate(); + } } rotPrev = st.rotate; // Horizontal @@ -1112,7 +1128,7 @@ private: Bag bag; ScoreState score; bool running = true, paused = false, touchingGround = false; - bool rotPrev = false, lHeld = false, rHeld = false, backPrev = false; + bool rotPrev = false, lHeld = false, rHeld = false, backPrev = false, selectPrev = false; uint32_t lHoldStart = 0, rHoldStart = 0, lLastRep = 0, rLastRep = 0, lastFall = 0, touchTime = 0; int current = 0, nextPiece = 0, px = 3, py = -2, rot = 0; // Game over restart gating @@ -1208,6 +1224,7 @@ private: gameOverTime = clock.millis(); gameOverPrevPressed = true; // require a release after delay // slow mode applied centrally next step + Buzzer::get().beepGameOver(); } } @@ -1243,7 +1260,9 @@ private: if (!lHeld) { lHeld = true; rHeld = false; - (void) tryMoveInternal(-1, 0); + if (tryMoveInternal(-1, 0)) { + Buzzer::get().beepMove(); + } lHoldStart = now; lLastRep = now; } else { @@ -1259,7 +1278,9 @@ private: if (!rHeld) { rHeld = true; lHeld = false; - (void) tryMoveInternal(+1, 0); + if (tryMoveInternal(+1, 0)) { + Buzzer::get().beepMove(); + } rHoldStart = now; rLastRep = now; } else { @@ -1289,6 +1310,7 @@ private: gameOverTime = clock.millis(); gameOverPrevPressed = true; // slow mode applied centrally next step + Buzzer::get().beepGameOver(); return; } int c = board.clearLines(); @@ -1300,7 +1322,12 @@ private: if (nl != score.level) { score.level = nl; score.dropMs = std::max(cfg::DropMsMin, cfg::DropMsStart - score.level * 50); + Buzzer::get().beepLevelUp(score.level); } + Buzzer::get().beepLines(c); + } + else { + Buzzer::get().beepLock(); } spawn(); } @@ -1360,6 +1387,7 @@ extern "C" void app_main() { SpiGlobal::get(); SMD::init(); DispTools::clear(); + Buzzer::get().init(); static PlatformFramebuffer fb; static PlatformInput input; diff --git a/Firmware/main/src/buzzer.cpp b/Firmware/main/src/buzzer.cpp new file mode 100644 index 0000000..156b7d8 --- /dev/null +++ b/Firmware/main/src/buzzer.cpp @@ -0,0 +1,191 @@ +// Buzzer implementation +#include "buzzer.hpp" +#include "config.hpp" + +#include +#include +#include +#include +#include + +static constexpr ledc_mode_t LEDC_MODE = LEDC_LOW_SPEED_MODE; // low speed is fine +static constexpr ledc_timer_t LEDC_TIMER = LEDC_TIMER_0; +static constexpr ledc_channel_t LEDC_CH = LEDC_CHANNEL_0; +static constexpr ledc_timer_bit_t LEDC_BITS = LEDC_TIMER_10_BIT; + +Buzzer &Buzzer::get() { + static Buzzer b; + return b; +} + +void Buzzer::init() { + // Initialize NVS once (safe if already done) + static bool nvsInited = false; + if (!nvsInited) { + esp_err_t err = nvs_flash_init(); + if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { + nvs_flash_erase(); + nvs_flash_init(); + } + nvsInited = true; + } + ledc_timer_config_t tcfg{}; + tcfg.speed_mode = LEDC_MODE; + tcfg.timer_num = LEDC_TIMER; + tcfg.duty_resolution = LEDC_BITS; + tcfg.freq_hz = 1000; // placeholder, changed per tone + tcfg.clk_cfg = LEDC_AUTO_CLK; + ESP_ERROR_CHECK(ledc_timer_config(&tcfg)); + + ledc_channel_config_t ccfg{}; + ccfg.speed_mode = LEDC_MODE; + ccfg.channel = LEDC_CH; + ccfg.timer_sel = LEDC_TIMER; + ccfg.gpio_num = static_cast(BUZZER_PIN); + ccfg.duty = 0; // start silent + ccfg.hpoint = 0; + ccfg.intr_type = LEDC_INTR_DISABLE; + ESP_ERROR_CHECK(ledc_channel_config(&ccfg)); + + esp_timer_create_args_t args{}; + args.callback = &Buzzer::timerCb; + args.arg = this; + args.name = "buzz"; + ESP_ERROR_CHECK(esp_timer_create(&args, reinterpret_cast(&_timer))); + loadState(); +} + +void Buzzer::applyFreq(uint32_t freq) { + if (freq == 0) { + ledc_stop(LEDC_MODE, LEDC_CH, 0); + return; + } + ledc_set_freq(LEDC_MODE, LEDC_TIMER, freq); + ledc_set_duty(LEDC_MODE, LEDC_CH, (1 << LEDC_BITS) / 2); + ledc_update_duty(LEDC_MODE, LEDC_CH); +} + +void Buzzer::enqueue(const Step &s) { + int nextTail = (_q_tail + 1) % MAX_QUEUE; + if (nextTail == _q_head) { // full, drop oldest + _q_head = (_q_head + 1) % MAX_QUEUE; + } + _queue[_q_tail] = s; + _q_tail = nextTail; +} + +void Buzzer::popFront() { + if (!empty()) + _q_head = (_q_head + 1) % MAX_QUEUE; +} + +void Buzzer::startNext() { + if (empty()) { + _running = false; + applyFreq(0); + return; + } + _running = true; + _in_gap = false; + Step &s = front(); + applyFreq(s.freq); + schedule(s.dur_ms, false); +} + +void Buzzer::schedule(uint32_t ms, bool gapPhase) { + if (!_timer) return; + _in_gap = gapPhase; + esp_timer_stop(reinterpret_cast(_timer)); + esp_timer_start_once(reinterpret_cast(_timer), (uint64_t)ms * 1000ULL); +} + +void Buzzer::timerCb(void *arg) { + auto *self = static_cast(arg); + if (!self) return; + if (self->_in_gap) { + self->popFront(); + self->startNext(); + return; + } + // Tone finished + if (!self->empty()) { + auto &s = self->front(); + if (s.gap_ms) { + self->applyFreq(0); + self->schedule(s.gap_ms, true); + return; + } + self->popFront(); + self->startNext(); + } +} + +void Buzzer::tone(uint32_t freq, uint32_t duration_ms, uint32_t gap_ms) { + if (_muted) return; // ignore while muted + Step s{freq, duration_ms, gap_ms}; + enqueue(s); + if (!_running) + startNext(); +} + +// ---- Game SFX ---- +void Buzzer::beepRotate() { tone(1800, 25); } +void Buzzer::beepMove() { tone(1200, 12); } +void Buzzer::beepLock() { tone(900, 25); } +void Buzzer::beepLines(int lines) { + static const uint32_t base = 1100; + for (int i = 0; i < lines; ++i) { + tone(base + i * 190, 40, 12); + } +} +void Buzzer::beepLevelUp(int) { + tone(1600, 70, 25); + tone(2000, 90, 0); +} +void Buzzer::beepGameOver() { + tone(1000, 140, 40); + tone(700, 140, 40); + tone(400, 260, 0); +} + +void Buzzer::setMuted(bool m) { + if (m == _muted) return; + _muted = m; + if (_muted) { + clearQueue(); + applyFreq(0); + if (_timer) { + esp_timer_stop(reinterpret_cast(_timer)); + } + _running = false; + _in_gap = false; + } else { + // confirmation chirp + tone(1500, 40, 10); + tone(1900, 60, 0); + } + saveState(); +} + +void Buzzer::toggleMuted() { setMuted(!_muted); } + +void Buzzer::loadState() { + nvs_handle_t h; + if (nvs_open("cfg", NVS_READONLY, &h) == ESP_OK) { + uint8_t v = 0; + if (nvs_get_u8(h, "mute", &v) == ESP_OK) { + _muted = (v != 0); + } + nvs_close(h); + } + if (_muted) applyFreq(0); +} + +void Buzzer::saveState() { + nvs_handle_t h; + if (nvs_open("cfg", NVS_READWRITE, &h) == ESP_OK) { + nvs_set_u8(h, "mute", _muted ? 1 : 0); + nvs_commit(h); + nvs_close(h); + } +}