boothifier/include/RtttlPlayer.h
2025-09-07 23:38:56 -07:00

327 lines
9.6 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#pragma once
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include <freertos/semphr.h>
// Tiny, efficient RTTTL player for ESP32 LEDC (non-blocking, priority-aware)
class RtttlPlayer {
public:
// pin: GPIO to output tone, channel: LEDC channel [0..15]
// timer: LEDC timer index [0..3], resolutionBits typically 10-13
RtttlPlayer(uint8_t pin, uint8_t channel, uint8_t timer=0, uint8_t resolutionBits=10,
uint16_t queueDepth=4)
: _pin(pin), _ch(channel), _timer(timer), _resBits(resolutionBits) {
// LEDC init
ledcSetup(_ch, /*freq*/ 1000, _resBits);
ledcAttachPin(_pin, _ch);
ledcWrite(_ch, 0); // duty 0 initially
// Sync primitives
_queue = xQueueCreate(queueDepth, sizeof(PlayItem));
_mtx = xSemaphoreCreateMutex();
_stopFlag = false;
_playing = false;
_curPrio = 0;
_preempt.item = nullptr; // Initialize preempt slot
// Playback task
xTaskCreatePinnedToCore(_taskThunk, "rtttl_task", 2048, this, /*prio*/ 1, &_taskHandle, 1);
}
~RtttlPlayer() {
if (_taskHandle) vTaskDelete(_taskHandle);
if (_queue) vQueueDelete(_queue);
if (_mtx) vSemaphoreDelete(_mtx);
}
// Non-blocking request to play a tune at 'priority'.
// Copies the RTTTL string internally (heap); returns false if queue full or alloc fails.
bool play(const char* rtttl, uint8_t priority) {
if (!rtttl) return false;
const size_t len = strnlen(rtttl, 512); // Reduce memory cap for efficiency
char* buf = (char*)malloc(len + 1);
if (!buf) return false;
memcpy(buf, rtttl, len);
buf[len] = '\0';
PlayItem item{buf, priority};
// Fast path: preempt if strictly higher priority than current
xSemaphoreTake(_mtx, portMAX_DELAY);
const bool shouldPreempt = _playing && (priority > _curPrio);
if (shouldPreempt) {
// Put new item into the front by sending to a small high-prio queue slot
// Approach: set preempt slot; playback loop will pick it ASAP.
if (_preempt.item) {
// Drop older preempt request to avoid leaks, keep newest
free((void*)_preempt.item->tune);
delete _preempt.item;
}
_preempt.item = new PlayItem(item); // copy
xSemaphoreGive(_mtx);
// we own original 'buf' no longer (copied into new PlayItem); free the temporary
free(buf);
// Signal stop to current note so task can switch between notes (cheap, cooperative)
_stopFlag = true;
return true;
}
xSemaphoreGive(_mtx);
// Otherwise enqueue and return (will play after current/earlier)
if (xQueueSend(_queue, &item, 0) == pdTRUE) {
return true;
} else {
free(buf);
return false;
}
}
// Stop playback immediately and flush queue (non-blocking).
void stopAll() {
xSemaphoreTake(_mtx, portMAX_DELAY);
_stopFlag = true;
// Drain queue
PlayItem tmp;
while (xQueueReceive(_queue, &tmp, 0) == pdTRUE) {
if (tmp.tune) free((void*)tmp.tune);
}
// Drop any pending preempt
if (_preempt.item) {
free((void*)_preempt.item->tune);
delete _preempt.item;
_preempt.item = nullptr;
}
xSemaphoreGive(_mtx);
}
private:
struct PlayItem {
const char* tune; // heap-allocated copy
uint8_t priority;
};
struct PreemptSlot {
PlayItem* item = nullptr; // one-slot "front of line"
};
// ===== LEDC helpers =====
inline void toneOn(uint32_t freq) {
if (freq == 0) { // pause
ledcWrite(_ch, 0);
return;
}
// Update LEDC frequency efficiently
// Use ledcSetup to set frequency (works on all Arduino-ESP32 versions)
ledcSetup(_ch, freq, _resBits);
// Duty: ~50% for square-like tone
const uint32_t dutyMax = (1U << _resBits) - 1U;
ledcWrite(_ch, dutyMax / 2);
}
inline void toneOff() {
ledcWrite(_ch, 0);
}
// ===== Playback task =====
static void _taskThunk(void* arg) {
((RtttlPlayer*)arg)->_taskLoop();
}
void _taskLoop() {
for (;;) {
// First check preempt slot, then queue
PlayItem item{};
if (_takePreempt(item) || xQueueReceive(_queue, &item, portMAX_DELAY) == pdTRUE) {
_playing = true;
_curPrio = item.priority;
_stopFlag = false;
_playOne(item.tune);
// cleanup
free((void*)item.tune);
_playing = false;
_curPrio = 0;
}
}
}
bool _takePreempt(PlayItem& out) {
bool got = false;
xSemaphoreTake(_mtx, portMAX_DELAY);
if (_preempt.item) {
out = *_preempt.item;
delete _preempt.item;
_preempt.item = nullptr;
got = true;
}
xSemaphoreGive(_mtx);
return got;
}
// ===== RTTTL parsing & playback =====
struct Defaults { uint16_t dur = 4; uint8_t oct = 5; uint16_t bpm = 63; };
// Note frequencies for octave 4 (rounded). Others are scaled by powers of two.
// Index by semitone: C, C#, D, D#, E, F, F#, G, G#, A, A#, B
static constexpr uint16_t baseA4 = 440;
// Well derive semitone freq using integer math: f = 440 * 2^((n)/12)
// To avoid floating point in the loop, we precompute a small LUT for octave 4.
static constexpr uint16_t LUT4[12] = {
262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494
};
static uint32_t freqFor(uint8_t noteIndex, int8_t octave) {
// noteIndex 0..11; octave typical 3..7
// Get octave 4 freq and scale by 2^(oct-4)
uint32_t f = LUT4[noteIndex];
if (octave > 4) f <<= (octave - 4);
else if (octave < 4) f >>= (4 - octave);
return f;
}
// Returns next token start or nullptr when finished
static const char* skipSpaces(const char* p) {
while (p && *p && (*p == ' ' || *p == ',')) ++p;
return p;
}
void _playOne(const char* rtttl) {
if (!rtttl) return;
Defaults def;
// Format: name:d=4,o=5,b=120: note[,note...]
const char* p = strchr(rtttl, ':');
if (!p) return;
const char* p2 = strchr(p + 1, ':');
if (!p2) return;
// Parse defaults between first and second ':'
parseDefaults(p + 1, p2, def);
// Whole note duration in ms
uint32_t wholenote = (60000UL / def.bpm) * 4;
// Notes section
p = p2 + 1;
p = skipSpaces(p);
while (p && *p) {
// Cooperative preemption between notes
if (_checkPreempt()) break;
uint16_t duration = 0;
// 1) Optional duration number (e.g., 16)
while (*p >= '0' && *p <= '9') {
duration = duration * 10 + (*p - '0');
++p;
}
if (duration == 0) duration = def.dur;
// 2) Note letter or pause
bool pause = false;
uint8_t noteIndex = 0xFF;
switch (tolower(*p)) {
case 'c': noteIndex = 0; break;
case 'd': noteIndex = 2; break;
case 'e': noteIndex = 4; break;
case 'f': noteIndex = 5; break;
case 'g': noteIndex = 7; break;
case 'a': noteIndex = 9; break;
case 'b': noteIndex = 11; break;
case 'p': pause = true; break;
default: break;
}
if (*p) ++p;
// 3) Optional sharp '#'
if (!pause && *p == '#') {
if (noteIndex != 0xFF) noteIndex++;
++p;
}
// 4) Optional dotted note '.'
bool dotted = false;
if (*p == '.') { dotted = true; ++p; }
// 5) Optional octave number
int8_t octave = def.oct;
if (*p >= '4' && *p <= '7') { octave = (*p - '0'); ++p; }
// Compute duration in ms
uint32_t noteDur = wholenote / duration;
if (dotted) noteDur += noteDur / 2;
// Play the note
if (pause || noteIndex == 0xFF) {
toneOff();
vTaskDelay(pdMS_TO_TICKS(noteDur));
} else {
const uint32_t f = freqFor(noteIndex % 12, octave);
toneOn(f);
// Shorten a little to add a tiny gap (staccato for clarity & queue responsiveness)
uint32_t onMs = (noteDur >= 4) ? (noteDur - 2) : noteDur;
uint32_t offMs = noteDur - onMs;
vTaskDelay(pdMS_TO_TICKS(onMs));
toneOff();
if (offMs) vTaskDelay(pdMS_TO_TICKS(offMs));
}
p = skipSpaces(p);
// Optional trailing comma already handled by skipSpaces
}
toneOff();
}
void parseDefaults(const char* beg, const char* end, Defaults& def) {
const char* p = beg;
while (p < end && *p) {
// key=value (d,o,b)
char key = tolower(*p);
const char* eq = (const char*)memchr(p, '=', end - p);
if (!eq) break;
const char* val = eq + 1;
const char* nxt = (const char*)memchr(val, ',', end - val);
if (!nxt) nxt = end;
uint32_t v = 0;
for (const char* t = val; t < nxt; ++t) {
if (*t >= '0' && *t <= '9') v = v * 10 + (*t - '0');
}
if (key == 'd' && v) def.dur = v;
else if (key == 'o' && v) def.oct = (uint8_t)v;
else if (key == 'b' && v) def.bpm = v;
p = (nxt < end) ? (nxt + 1) : end;
}
// Clamp sane ranges
if (def.dur == 0) def.dur = 4;
if (def.oct < 3 || def.oct > 7) def.oct = 5;
if (def.bpm < 20) def.bpm = 20;
if (def.bpm > 400) def.bpm = 400;
}
bool _checkPreempt() {
if (!_stopFlag) return false;
// Grab preempt immediately if present, otherwise just stop current tune
PlayItem nxt{};
if (_takePreempt(nxt)) {
// Play preempted tune immediately (no recursion - direct call)
_curPrio = nxt.priority;
_stopFlag = false;
_playOne(nxt.tune);
free((void*)nxt.tune);
}
// Indicate current tune should finish early
return true;
}
// ===== members =====
uint8_t _pin, _ch, _timer, _resBits;
TaskHandle_t _taskHandle = nullptr;
QueueHandle_t _queue = nullptr;
SemaphoreHandle_t _mtx = nullptr;
volatile bool _stopFlag;
volatile bool _playing;
volatile uint8_t _curPrio;
PreemptSlot _preempt;
};