feat(ui-core,ui-lvgl): sound in the Core, L2 speaker via ES8311, Sound settings, melody editor

- genericBuzzer I2S path (BUZZER_I2S): an audio task synthesises the notes
  with eased edges and keeps the DMA queue fed with silence between sounds;
  ES8311 setup; amp powered through the board only around sounds
- ui-core/SoundControl.h (mode, volume, built-in sounds, melody model),
  SoundNotifier moved to the Core, chanctl::melody, schema Sound page
- L2: notifications, alarm / timer ring, locator sounds, Settings > Sound,
  melody editor with the playing note lit, per-chat sound, mute icon
- noteIndex() on every buzzer backend; scrolling popups past screen height

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-26 00:27:52 +02:00
co-authored by Claude Opus 5.5
parent 2624ab0548
commit 938dfefba6
25 changed files with 1335 additions and 151 deletions
+17 -26
View File
@@ -1,5 +1,6 @@
#include "UITask.h"
#include "SoundNotifier.h"
#include "../ui-core/SoundNotifier.h"
#include "../ui-core/SoundControl.h"
#include <helpers/TxtDataHelpers.h>
#include "../MyMesh.h"
#include "../MsgExpand.h"
@@ -1667,9 +1668,9 @@ void UITask::gotoGpioScreen() {
void UITask::gotoLiveShareScreen() { setCurrScreen(live_share_screen); }
// ── Clock tools (engine in ui-core/ClockEngine.h) ───────────────────────────
// The melody overrides mute (playMelody → buzzer.playForced); the ring
// auto-stops after ClockEngine::RING_MS if no key dismisses it (see loop()).
static const char* CLOCK_ALARM_MELODY = "alarm:d=8,o=6,b=125:c,c,c,c,p,c,c,c,c,p";
// The melody (soundctl::MEL_ALARM) overrides mute (playMelody →
// buzzer.playForced); the ring auto-stops after ClockEngine::RING_MS if no key
// dismisses it (see loop()).
void UITask::wakeForAlarm() {
if (_display != NULL) _display->turnOn();
@@ -1693,7 +1694,7 @@ void UITask::tickCore() {
_core->loop();
drainCoreEvents();
// Repeat the ring melody until dismissed or the ring window elapses.
if (_core->clock.isRinging() && !isMelodyPlaying()) playMelody(CLOCK_ALARM_MELODY);
if (_core->clock.isRinging() && !isMelodyPlaying()) playMelody(soundctl::MEL_ALARM);
}
void UITask::drainCoreEvents() {
@@ -1703,7 +1704,7 @@ void UITask::drainCoreEvents() {
case UiEventType::ClockAlert:
wakeForAlarm();
showAlert(ev.text, ClockEngine::RING_MS);
playMelody(CLOCK_ALARM_MELODY);
playMelody(soundctl::MEL_ALARM);
break;
case UiEventType::ClockRingEnded:
stopMelody();
@@ -1715,10 +1716,10 @@ void UITask::drainCoreEvents() {
case UiEventType::LocatorCrossed:
showAlert(ev.text, 3000);
if (!isBuzzerQuiet())
playMelody(ev.flag ? "locarr:d=8,o=6,b=140:c,e,g" : "loclv:d=8,o=6,b=140:g,e,c");
playMelody(ev.flag ? soundctl::MEL_ARRIVE : soundctl::MEL_LEAVE);
break;
case UiEventType::LocatorBeep:
playMelody("locp:d=32,o=7,b=200:c");
playMelody(soundctl::MEL_TICK);
break;
case UiEventType::MessageArrived:
onMessageArrived(ev);
@@ -2681,13 +2682,8 @@ void UITask::loop() {
userLedHandler();
#ifdef PIN_BUZZER
if (_node_prefs && _node_prefs->buzzer_auto) {
bool should_quiet = isClientConnected(); // BLE bonded or an open USB port
if (buzzer.isQuiet() != should_quiet) {
buzzer.quiet(should_quiet);
_next_refresh = 0;
}
}
if (soundctl::autoTick(_node_prefs, buzzer, isClientConnected())) // BLE bonded or an open USB port
_next_refresh = 0;
if (buzzer.isPlaying()) buzzer.loop();
#endif
@@ -3257,10 +3253,7 @@ void UITask::setBrightnessLevel(uint8_t level) {
void UITask::setBuzzerVolumeLevel(uint8_t level) {
#ifdef PIN_BUZZER
if (_node_prefs == NULL) return;
if (level > 4) level = 4;
_node_prefs->buzzer_volume = level;
buzzer.setVolume(level);
if (level > 0) buzzer.playForced("Vol:d=16,o=6,b=120:c");
soundctl::setVolume(_node_prefs, buzzer, level);
_next_refresh = 0;
#endif
}
@@ -3283,8 +3276,8 @@ void UITask::toggleBuzzer() {
int UITask::getBuzzerMode() {
#ifdef PIN_BUZZER
if (_node_prefs && _node_prefs->buzzer_auto) return 2;
return buzzer.isQuiet() ? 1 : 0;
if (_node_prefs && _node_prefs->buzzer_auto) return soundctl::MODE_AUTO;
return buzzer.isQuiet() ? soundctl::MODE_OFF : soundctl::MODE_ON;
#else
return 1;
#endif
@@ -3294,11 +3287,9 @@ void UITask::cycleBuzzerMode() {
#ifdef PIN_BUZZER
if (!_node_prefs) return;
int mode = getBuzzerMode();
mode = (mode + 1) % 3; // ON(0) → OFF(1) → Auto(2) → ON
_node_prefs->buzzer_auto = (mode == 2) ? 1 : 0;
if (mode == 0) { buzzer.quiet(false); _node_prefs->buzzer_quiet = 0; notify(UIEventType::ack); }
if (mode == 1) { buzzer.quiet(true); _node_prefs->buzzer_quiet = 1; }
if (mode == 2) { buzzer.quiet(isClientConnected()); }
mode = (mode + 1) % soundctl::MODE_COUNT; // ON → OFF → Auto → ON
soundctl::setMode(_node_prefs, buzzer, (uint8_t)mode, isClientConnected());
if (mode == soundctl::MODE_ON) notify(UIEventType::ack);
static const char* labels[] = { "Buzzer: ON", "Buzzer: OFF", "Buzzer: Auto" };
showAlert(labels[mode], 800);
_next_refresh = 0;