mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 15:17:48 +01:00
buzzer
This commit is contained in:
@@ -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")
|
||||
|
||||
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_HEIGHT 240
|
||||
|
||||
#define BUZZER_PIN GPIO_NUM_25
|
||||
|
||||
#define PWR_INT GPIO_NUM_10
|
||||
#define PWR_KILL GPIO_NUM_12
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
// Battery monitor header (conditionally included)
|
||||
#include <bat_mon.hpp>
|
||||
#include "power_helper.hpp"
|
||||
#include <buzzer.hpp>
|
||||
|
||||
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;
|
||||
|
||||
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