Add Zira spoken button confirmations and opt-in G726 speech tests

This commit is contained in:
mikecarper committed 2026-10-03 03:03:02 -07:00
1 parent 5833ac2f1f
commit 8519638367
33 files changed
+11977 -30

No files matched your search

+183 -2
View File
@@ -1,6 +1,51 @@
#include "Arduino.h"
#ifdef PIN_BUZZER
#include "buzzer.h"
#if MESH_GPS_VOICE
#if defined(MESH_GPS_VOICE_G726_BITRATE)
// Board source filters already compile this player entry point. Keep the
// experimental decoder out of ordinary images and avoid changing every
// board's filter just for the hardware comparison.
#include "g726/g726_decoder.c"
#include "G726VoiceData.h"
#else
#include "ButtonVoiceData.h"
#include "GpsVoiceData.h"
#endif
#include "nrf52/GpsVoicePlayer.h"
static mesh::audio::GpsVoicePlayer gps_voice;
extern "C" void PWM3_IRQHandler() { gps_voice.interrupt(); }
static mesh::audio::VoiceClip buttonVoiceClip(ButtonVoicePrompt prompt) {
using namespace mesh::audio;
switch (prompt) {
case ButtonVoicePrompt::Ready: return readyClip;
case ButtonVoicePrompt::NotificationCleared: return notificationClearedClip;
case ButtonVoicePrompt::AdvertQueued: return advertQueuedClip;
case ButtonVoicePrompt::AdvertFailed: return advertFailedClip;
case ButtonVoicePrompt::SoundOn: return soundOnClip;
case ButtonVoicePrompt::SoundOff: return soundOffClip;
case ButtonVoicePrompt::GpsOn: return gpsOnClip;
case ButtonVoicePrompt::GpsOff: return gpsOffClip;
case ButtonVoicePrompt::ActionFailed: return actionFailedClip;
case ButtonVoicePrompt::UsbSetup: return usbSetupClip;
case ButtonVoicePrompt::ShuttingDown: return shuttingDownClip;
case ButtonVoicePrompt::Restarting: return restartingClip;
#ifdef HAS_DRV2605
case ButtonVoicePrompt::AlertsSoundVibration: return alertsSoundVibrationClip;
case ButtonVoicePrompt::AlertsSoundOnly: return alertsSoundOnlyClip;
case ButtonVoicePrompt::AlertsVibrationOnly: return alertsVibrationOnlyClip;
case ButtonVoicePrompt::AlertsSilent: return alertsSilentClip;
#else
case ButtonVoicePrompt::AlertsSoundVibration:
case ButtonVoicePrompt::AlertsSoundOnly:
case ButtonVoicePrompt::AlertsVibrationOnly:
case ButtonVoicePrompt::AlertsSilent: return {};
#endif
}
return {};
}
#endif
void genericBuzzer::begin() {
// Serial.print("DBG: Setting up buzzer on pin ");
@@ -16,9 +61,14 @@ void genericBuzzer::begin() {
}
void genericBuzzer::play(const char *melody) {
#if MESH_GPS_VOICE
// A muted ordinary alert must not cancel the one courtesy confirmation
// that may still be queued or playing while the preference is quiet.
if (_is_quiet) return;
#endif
if (isPlaying()) // interrupt existing
{
rtttl::stop();
stop();
}
if (_is_quiet) return;
@@ -36,19 +86,149 @@ void genericBuzzer::playNotification(const char* melody) {
rtttl::begin(PIN_BUZZER, melody);
}
void genericBuzzer::stopNotification() {
#if MESH_GPS_VOICE
// Notification timers own their tones, not user-requested confirmations.
// Preserve a pending direction tone, speech gap, or active voice clip.
if (_voice_pending || gps_voice.active()) return;
#endif
stop();
}
bool genericBuzzer::speakGps(bool enabled) {
return speakButton(enabled ? ButtonVoicePrompt::GpsOn : ButtonVoicePrompt::GpsOff);
}
bool genericBuzzer::speakButton(ButtonVoicePrompt prompt, bool allowQuiet) {
const char* melody = nullptr;
switch (prompt) {
case ButtonVoicePrompt::GpsOn:
melody = startup_song;
break;
case ButtonVoicePrompt::GpsOff:
case ButtonVoicePrompt::ShuttingDown:
case ButtonVoicePrompt::Restarting:
melody = shutdown_song;
break;
case ButtonVoicePrompt::Ready:
case ButtonVoicePrompt::NotificationCleared:
case ButtonVoicePrompt::AdvertQueued:
case ButtonVoicePrompt::AdvertFailed:
case ButtonVoicePrompt::SoundOn:
case ButtonVoicePrompt::SoundOff:
case ButtonVoicePrompt::ActionFailed:
case ButtonVoicePrompt::UsbSetup:
case ButtonVoicePrompt::AlertsSoundVibration:
case ButtonVoicePrompt::AlertsSoundOnly:
case ButtonVoicePrompt::AlertsVibrationOnly:
case ButtonVoicePrompt::AlertsSilent:
#if MESH_GPS_VOICE && !defined(HAS_DRV2605)
// Reject unavailable modes before cancelling any valid audio.
if (prompt >= ButtonVoicePrompt::AlertsSoundVibration) return false;
#endif
break;
default:
return false;
}
stop();
if (_is_quiet && !allowQuiet) return true;
#if !MESH_GPS_VOICE
// The UI retains its legacy acknowledgements on untested boards. New
// voice-only prompts must not introduce tones there (including Ready).
if (!melody) return true;
#endif
#ifdef PIN_BUZZER_EN
digitalWrite(PIN_BUZZER_EN, HIGH);
#endif
#if MESH_GPS_VOICE
_voice_prompt = prompt;
_voice_pending = true;
_voice_allow_quiet = allowQuiet;
_voice_gap_started = false;
#endif
// Preserve the recognisable rising/falling control tone even where voice
// playback is disabled. Optional tones release their PWM before speech.
if (melody) rtttl::begin(PIN_BUZZER, melody);
return true;
}
#ifdef MESH_BUTTON_AUDIO_HIL
void genericBuzzer::voiceStatus(char* reply, size_t size) {
#if MESH_GPS_VOICE
snprintf(reply, size, "voice playing=%u started=%lu completed=%lu cancelled=%lu samples=%lu quiet=%u gain=%u pending=%u tone=%u",
gps_voice.playing(), (unsigned long)gps_voice.started(),
(unsigned long)gps_voice.completed(), (unsigned long)gps_voice.cancelled(),
(unsigned long)gps_voice.played(), _is_quiet, gps_voice.gain(),
_voice_pending, rtttl::isPlaying());
#else
snprintf(reply, size, "voice unsupported");
#endif
}
bool genericBuzzer::voiceGain(uint8_t gain) {
#if MESH_GPS_VOICE
return !_voice_pending && gps_voice.setGain(gain);
#else
(void)gain;
return false;
#endif
}
#endif
void genericBuzzer::stop() {
rtttl::stop();
#if MESH_GPS_VOICE
_voice_pending = false;
_voice_allow_quiet = false;
_voice_gap_started = false;
gps_voice.stop();
#endif
#ifdef PIN_BUZZER_EN
if (_is_quiet) digitalWrite(PIN_BUZZER_EN, LOW);
#endif
}
bool genericBuzzer::isPlaying() {
#if MESH_GPS_VOICE
if (_voice_pending || gps_voice.active()) return true;
#endif
return rtttl::isPlaying();
}
void genericBuzzer::loop() {
if (!rtttl::done()) rtttl::play();
#if MESH_GPS_VOICE
gps_voice.service();
if (gps_voice.playing()) return;
if (_voice_pending) {
if (!rtttl::done()) {
rtttl::play();
return;
}
if (!_voice_gap_started) {
rtttl::stop();
_voice_gap_ms = millis();
_voice_gap_started = true;
}
if (static_cast<uint32_t>(millis() - _voice_gap_ms) < 100) return;
_voice_pending = false;
_voice_gap_started = false;
if (!_is_quiet || _voice_allow_quiet)
gps_voice.start(buttonVoiceClip(_voice_prompt));
_voice_allow_quiet = false;
if (gps_voice.playing()) return;
}
#endif
if (!rtttl::done()) {
rtttl::play();
return;
}
// Courtesy mute/unmute speech can temporarily power the driver while the
// preference stays quiet. Return both pins to their low-power state.
if (_is_quiet) {
digitalWrite(PIN_BUZZER, LOW);
#ifdef PIN_BUZZER_EN
digitalWrite(PIN_BUZZER_EN, LOW);
#endif
}
}
void genericBuzzer::startup() {
@@ -60,6 +240,7 @@ void genericBuzzer::shutdown() {
}
void genericBuzzer::quiet(bool buzzer_state) {
if (buzzer_state) stop();
_is_quiet = buzzer_state;
#ifdef PIN_BUZZER_EN
if (_is_quiet) {