feat(sim): real buzzer/RTTTL sound via Web Audio

genericBuzzer (src/helpers/ui/buzzer.h/.cpp) gets a third platform branch,
#elif defined(SIM_PLATFORM), alongside the existing NRF52 (direct PWM) and
NonBlockingRtttl paths -- purely additive, no changes to either real-hardware
branch. It reuses the NRF52 branch's already hardware-free RTTTL parser
(_parseHeader/_parseNext/_noteFreq, now shared via a widened guard) but
tracks (current frequency, note-end-time) instead of touching real PWM/timer
registers, advancing on plain millis() polling from loop() -- same
non-blocking shape UITask already drives every tick.

Wired into the sim build the same way every real board sets its buzzer pin
(-D PIN_BUZZER=<n> in build_flags/DEFINES; here it's a dummy sentinel since
there's no real pin, just something to activate the existing #ifdef
PIN_BUZZER guards in UITask.h/.cpp/SoundNotifier.h unchanged), plus two new
small UITask accessors (isBuzzerPlaying/buzzerFreqHz/buzzerVolume) and three
EMSCRIPTEN_KEEPALIVE exports so a host page can poll the buzzer's state.

Browser side: one Web Audio oscillator+gain per companion instance (index.html
single-instance; mesh.html per A/B, not R which is headless), created lazily
on the first real user gesture (AudioContext autoplay policy), polled every
20ms and mapped to the oscillator frequency/gain -- so every notification
sound, ringtone, alarm, and volume-blip that already worked on real hardware
now actually produces audio in the browser, unchanged at the call-site level.

Verified end-to-end with real RTTTL playback traces (not just "no errors"):
the startup jingle's exact note frequencies (C6/E6/G6) and a real DM-received
notification triggering the receiving instance's Web Audio gain node from 0
to its mapped volume and back, matching the actual "MsgRcv3" melody's notes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-03 13:59:26 +02:00
co-authored by Claude Sonnet 5
parent 0cb02ee18f
commit f61d6832a7
8 changed files with 254 additions and 18 deletions
@@ -2157,6 +2157,23 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_enqueue_key(char c) {
extern "C" EMSCRIPTEN_KEEPALIVE void sim_enqueue_key_longpress(char c) {
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->injectSimKeyLongPress(c);
}
#ifdef PIN_BUZZER
// Polled by the host page every ~20ms (see web/index.html/mesh.html) to
// drive a Web Audio oscillator standing in for the real piezo buzzer --
// genericBuzzer's own #ifdef SIM_PLATFORM branch (src/helpers/ui/buzzer.cpp)
// tracks (is a note sounding, at what frequency) instead of touching real
// PWM/timer hardware; these two exports are just the read side of that.
extern "C" EMSCRIPTEN_KEEPALIVE int sim_buzzer_is_playing() {
return (g_sim_ui_task_for_js && g_sim_ui_task_for_js->isBuzzerPlaying()) ? 1 : 0;
}
extern "C" EMSCRIPTEN_KEEPALIVE int sim_buzzer_freq_hz() {
return g_sim_ui_task_for_js ? (int)g_sim_ui_task_for_js->buzzerFreqHz() : 0;
}
extern "C" EMSCRIPTEN_KEEPALIVE int sim_buzzer_get_volume() {
return g_sim_ui_task_for_js ? (int)g_sim_ui_task_for_js->buzzerVolume() : 0;
}
#endif
#endif
bool UITask::dequeueKey(char& c) {
+12
View File
@@ -227,6 +227,18 @@ public:
// never open the context menu at all -- injectSimKey() alone has no way
// to signal "this press was held".
void injectSimKeyLongPress(char c);
#ifdef PIN_BUZZER
// Lets a host page poll the buzzer's current state every frame to drive a
// Web Audio oscillator (see sim_buzzer_is_playing()/sim_buzzer_freq_hz()
// in main.cpp) -- `buzzer` itself is a private member, so a free function
// outside this class needs these to reach it, same reason injectSimKey()
// above is public.
// Not const: genericBuzzer::isPlaying() itself isn't const-qualified (its
// NRF52/non-NRF52 siblings don't need to be, so it wasn't worth widening).
bool isBuzzerPlaying() { return buzzer.isPlaying(); }
uint16_t buzzerFreqHz() const { return buzzer.currentFreqHz(); }
uint8_t buzzerVolume() const { return buzzer.getVolume(); }
#endif
private:
#endif