mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 23:27:49 +01:00
buzzer
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
54
Firmware/main/include/buzzer.hpp
Normal file
54
Firmware/main/include/buzzer.hpp
Normal 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; }
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
191
Firmware/main/src/buzzer.cpp
Normal file
191
Firmware/main/src/buzzer.cpp
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user