mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 23:27:49 +01:00
192 lines
5.0 KiB
C++
192 lines
5.0 KiB
C++
// Buzzer implementation
|
|
#include "cardboy/backend/esp/buzzer.hpp"
|
|
#include "cardboy/backend/esp/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);
|
|
}
|
|
}
|