This commit is contained in:
2025-10-07 22:50:45 +02:00
parent 413e021e49
commit ecf6d09651
5 changed files with 282 additions and 6 deletions

View File

@@ -9,5 +9,6 @@ idf_component_register(SRCS
src/shutdowner.cpp src/shutdowner.cpp
src/buttons.cpp src/buttons.cpp
src/power_helper.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") INCLUDE_DIRS "include")

View File

@@ -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 <cstdint>
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; }
};

View File

@@ -18,6 +18,8 @@
#define DISP_WIDTH 400 #define DISP_WIDTH 400
#define DISP_HEIGHT 240 #define DISP_HEIGHT 240
#define BUZZER_PIN GPIO_NUM_25
#define PWR_INT GPIO_NUM_10 #define PWR_INT GPIO_NUM_10
#define PWR_KILL GPIO_NUM_12 #define PWR_KILL GPIO_NUM_12

View File

@@ -39,6 +39,7 @@
// Battery monitor header (conditionally included) // Battery monitor header (conditionally included)
#include <bat_mon.hpp> #include <bat_mon.hpp>
#include "power_helper.hpp" #include "power_helper.hpp"
#include <buzzer.hpp>
namespace cfg { namespace cfg {
constexpr int BoardW = 10; constexpr int BoardW = 10;
@@ -75,7 +76,7 @@ struct IFramebuffer {
}; };
struct InputState { 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 { struct IInput {
virtual ~IInput() = default; virtual ~IInput() = default;
@@ -123,6 +124,8 @@ struct PlatformInput final : IInput {
s.rotate = true; // rotate s.rotate = true; // rotate
if (p & BTN_B) if (p & BTN_B)
s.back = true; // pause/back s.back = true; // pause/back
if (p & BTN_SELECT)
s.select = true; // mute toggle
return s; return s;
} }
}; };
@@ -829,6 +832,12 @@ private:
drawText(x, y, line1); drawText(x, y, line1);
drawText(x, y + 10, line2); drawText(x, y + 10, line2);
drawText(x, y + 20, line3); 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; dirty = true;
} }
backPrev = st.back; backPrev = st.back;
// Mute toggle (Select button)
if (st.select && !selectPrev) {
Buzzer::get().toggleMuted();
}
selectPrev = st.select;
if (paused) { if (paused) {
uint64_t logicEndUs = esp_timer_get_time(); uint64_t logicEndUs = esp_timer_get_time();
{ {
@@ -1005,8 +1019,10 @@ public:
} }
// Rotation // Rotation
if (st.rotate && !rotPrev) { if (st.rotate && !rotPrev) {
if (tryRotate(+1)) if (tryRotate(+1)) {
dirty = true; dirty = true;
Buzzer::get().beepRotate();
}
} }
rotPrev = st.rotate; rotPrev = st.rotate;
// Horizontal // Horizontal
@@ -1112,7 +1128,7 @@ private:
Bag bag; Bag bag;
ScoreState score; ScoreState score;
bool running = true, paused = false, touchingGround = false; 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; 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; int current = 0, nextPiece = 0, px = 3, py = -2, rot = 0;
// Game over restart gating // Game over restart gating
@@ -1208,6 +1224,7 @@ private:
gameOverTime = clock.millis(); gameOverTime = clock.millis();
gameOverPrevPressed = true; // require a release after delay gameOverPrevPressed = true; // require a release after delay
// slow mode applied centrally next step // slow mode applied centrally next step
Buzzer::get().beepGameOver();
} }
} }
@@ -1243,7 +1260,9 @@ private:
if (!lHeld) { if (!lHeld) {
lHeld = true; lHeld = true;
rHeld = false; rHeld = false;
(void) tryMoveInternal(-1, 0); if (tryMoveInternal(-1, 0)) {
Buzzer::get().beepMove();
}
lHoldStart = now; lHoldStart = now;
lLastRep = now; lLastRep = now;
} else { } else {
@@ -1259,7 +1278,9 @@ private:
if (!rHeld) { if (!rHeld) {
rHeld = true; rHeld = true;
lHeld = false; lHeld = false;
(void) tryMoveInternal(+1, 0); if (tryMoveInternal(+1, 0)) {
Buzzer::get().beepMove();
}
rHoldStart = now; rHoldStart = now;
rLastRep = now; rLastRep = now;
} else { } else {
@@ -1289,6 +1310,7 @@ private:
gameOverTime = clock.millis(); gameOverTime = clock.millis();
gameOverPrevPressed = true; gameOverPrevPressed = true;
// slow mode applied centrally next step // slow mode applied centrally next step
Buzzer::get().beepGameOver();
return; return;
} }
int c = board.clearLines(); int c = board.clearLines();
@@ -1300,7 +1322,12 @@ private:
if (nl != score.level) { if (nl != score.level) {
score.level = nl; score.level = nl;
score.dropMs = std::max(cfg::DropMsMin, cfg::DropMsStart - score.level * 50); 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(); spawn();
} }
@@ -1360,6 +1387,7 @@ extern "C" void app_main() {
SpiGlobal::get(); SpiGlobal::get();
SMD::init(); SMD::init();
DispTools::clear(); DispTools::clear();
Buzzer::get().init();
static PlatformFramebuffer fb; static PlatformFramebuffer fb;
static PlatformInput input; static PlatformInput input;

View File

@@ -0,0 +1,191 @@
// Buzzer implementation
#include "buzzer.hpp"
#include "config.hpp"
#include <driver/ledc.h>
#include <esp_err.h>
#include <esp_timer.h>
#include <nvs_flash.h>
#include <nvs.h>
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<int>(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<esp_timer_handle_t*>(&_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<esp_timer_handle_t>(_timer));
esp_timer_start_once(reinterpret_cast<esp_timer_handle_t>(_timer), (uint64_t)ms * 1000ULL);
}
void Buzzer::timerCb(void *arg) {
auto *self = static_cast<Buzzer*>(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<esp_timer_handle_t>(_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);
}
}