Skip to content

Commit 40fac02

Browse files
committed
feat(audio): add protocol-agnostic message prompt tones for pager and tdeck
1 parent bbd5b8c commit 40fac02

File tree

7 files changed

+185
-0
lines changed

7 files changed

+185
-0
lines changed

src/app/app_context.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ void AppContext::update()
294294
{
295295
APP_EVENT_LOG("[AppContext::update] Triggering haptic feedback...\n");
296296
board_->vibrator();
297+
board_->playMessageTone();
297298
APP_EVENT_LOG("[AppContext::update] Haptic feedback triggered\n");
298299
}
299300
else
@@ -311,6 +312,7 @@ void AppContext::update()
311312
if (board_)
312313
{
313314
board_->vibrator();
315+
board_->playMessageTone();
314316
}
315317
std::string notice = "Team: ";
316318
const auto& msg = team_event->data.msg;

src/board/BoardBase.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ class BoardBase
3737
// 触觉反馈
3838
virtual void vibrator() = 0;
3939
virtual void stopVibrator() = 0;
40+
41+
// 收到消息提示音(默认空实现,具体板级按需覆盖)
42+
virtual void playMessageTone() {}
4043
};
4144

4245
// 全局板实例(与原来的 instance 等价,用于解耦调用方类型)

src/board/TDeckBoard.cpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#include "board/TDeckBoard.h"
22
#include "board/sd_utils.h"
33
#include "display/drivers/ST7789TDeck.h"
4+
#include <AudioFileSourcePROGMEM.h>
5+
#include <AudioGeneratorRTTTL.h>
6+
#include <AudioOutputI2S.h>
47
#include <SD.h>
58
#include <Wire.h>
69
#include <ctime>
@@ -872,6 +875,57 @@ RotaryMsg_t TDeckBoard::getRotary()
872875
return msg;
873876
}
874877

878+
void TDeckBoard::playMessageTone()
879+
{
880+
#if defined(DAC_I2S_BCK) && defined(DAC_I2S_WS) && defined(DAC_I2S_DOUT)
881+
static bool s_playing = false;
882+
static uint32_t s_last_play_ms = 0;
883+
884+
if (s_playing)
885+
{
886+
return;
887+
}
888+
889+
const uint32_t now = millis();
890+
if ((now - s_last_play_ms) < 240)
891+
{
892+
return;
893+
}
894+
895+
s_playing = true;
896+
s_last_play_ms = now;
897+
898+
static const char kMessageToneRtttl[] = "MsgRcv:d=4,o=6,b=200:32e,32g,32b,16c7";
899+
900+
AudioOutputI2S audio_out(1, AudioOutputI2S::EXTERNAL_I2S);
901+
#if defined(DAC_I2S_MCLK)
902+
audio_out.SetPinout(DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT, DAC_I2S_MCLK);
903+
#else
904+
audio_out.SetPinout(DAC_I2S_BCK, DAC_I2S_WS, DAC_I2S_DOUT);
905+
#endif
906+
audio_out.SetGain(0.18f);
907+
908+
AudioFileSourcePROGMEM song(kMessageToneRtttl, sizeof(kMessageToneRtttl) - 1);
909+
AudioGeneratorRTTTL generator;
910+
911+
if (generator.begin(&song, &audio_out))
912+
{
913+
const uint32_t deadline = millis() + 1600;
914+
while (generator.isRunning() && millis() < deadline)
915+
{
916+
if (!generator.loop())
917+
{
918+
break;
919+
}
920+
delay(1);
921+
}
922+
generator.stop();
923+
}
924+
audio_out.stop();
925+
s_playing = false;
926+
#endif
927+
}
928+
875929
namespace
876930
{
877931
TDeckBoard& getInstanceRef()

src/board/TDeckBoard.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class TDeckBoard : public BoardBase,
6565

6666
void vibrator() override {}
6767
void stopVibrator() override {}
68+
void playMessageTone() override;
6869

6970
// LilyGo_Display
7071
void setRotation(uint8_t rotation) override;

src/board/TLoRaPagerBoard.cpp

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "freertos/task.h"
88
#include "freertos/timers.h"
99

10+
#include <cmath>
1011
#include <cstring>
1112
#include <driver/gpio.h>
1213
#include <esp_sleep.h>
@@ -859,6 +860,119 @@ void TLoRaPagerBoard::stopVibrator()
859860
log_d("[stopVibrator] Power disabled, function completed");
860861
}
861862

863+
void TLoRaPagerBoard::playMessageTone()
864+
{
865+
#ifndef USING_AUDIO_CODEC
866+
return;
867+
#else
868+
if (!(devices_probe & HW_CODEC_ONLINE))
869+
{
870+
return;
871+
}
872+
873+
static bool s_playing = false;
874+
static uint32_t s_last_play_ms = 0;
875+
876+
if (s_playing)
877+
{
878+
return;
879+
}
880+
881+
const uint32_t now = millis();
882+
if ((now - s_last_play_ms) < 240)
883+
{
884+
return;
885+
}
886+
887+
s_playing = true;
888+
s_last_play_ms = now;
889+
890+
struct ToneStep
891+
{
892+
uint16_t freq_hz;
893+
uint16_t duration_ms;
894+
};
895+
896+
static constexpr ToneStep kTone[] = {
897+
{1319, 55},
898+
{1568, 55},
899+
{1976, 90},
900+
};
901+
static constexpr uint16_t kGapMs = 18;
902+
static constexpr uint32_t kSampleRate = 16000;
903+
static constexpr uint8_t kChannels = 2;
904+
static constexpr size_t kFramesPerChunk = 128;
905+
static constexpr float kAmplitude = 0.16f;
906+
static constexpr float kTwoPi = 6.28318530718f;
907+
908+
int prev_volume = codec.getVolume();
909+
if (prev_volume < 0 || prev_volume > 100)
910+
{
911+
prev_volume = 50;
912+
}
913+
bool prev_out_mute = codec.getOutMute();
914+
915+
bool opened = (codec.open(16, kChannels, kSampleRate) == 0);
916+
if (!opened)
917+
{
918+
s_playing = false;
919+
return;
920+
}
921+
922+
codec.setOutMute(false);
923+
codec.setVolume(static_cast<uint8_t>(prev_volume < 45 ? 45 : prev_volume));
924+
925+
int16_t pcm[kFramesPerChunk * kChannels];
926+
auto write_silence = [&](uint32_t ms)
927+
{
928+
uint32_t remaining = (kSampleRate * ms) / 1000;
929+
while (remaining > 0)
930+
{
931+
size_t frames = remaining > kFramesPerChunk ? kFramesPerChunk : static_cast<size_t>(remaining);
932+
memset(pcm, 0, frames * kChannels * sizeof(int16_t));
933+
codec.write(reinterpret_cast<uint8_t*>(pcm), frames * kChannels * sizeof(int16_t));
934+
remaining -= static_cast<uint32_t>(frames);
935+
}
936+
};
937+
938+
for (size_t i = 0; i < (sizeof(kTone) / sizeof(kTone[0])); ++i)
939+
{
940+
const ToneStep& step = kTone[i];
941+
uint32_t remaining = (kSampleRate * step.duration_ms) / 1000;
942+
float phase = 0.0f;
943+
const float phase_step = (kTwoPi * static_cast<float>(step.freq_hz)) / static_cast<float>(kSampleRate);
944+
945+
while (remaining > 0)
946+
{
947+
size_t frames = remaining > kFramesPerChunk ? kFramesPerChunk : static_cast<size_t>(remaining);
948+
for (size_t n = 0; n < frames; ++n)
949+
{
950+
int16_t sample = static_cast<int16_t>(sinf(phase) * (32767.0f * kAmplitude));
951+
pcm[n * 2] = sample;
952+
pcm[n * 2 + 1] = sample;
953+
phase += phase_step;
954+
if (phase >= kTwoPi)
955+
{
956+
phase -= kTwoPi;
957+
}
958+
}
959+
codec.write(reinterpret_cast<uint8_t*>(pcm), frames * kChannels * sizeof(int16_t));
960+
remaining -= static_cast<uint32_t>(frames);
961+
}
962+
963+
if (i + 1 < (sizeof(kTone) / sizeof(kTone[0])))
964+
{
965+
write_silence(kGapMs);
966+
}
967+
}
968+
969+
codec.setVolume(static_cast<uint8_t>(prev_volume));
970+
codec.setOutMute(prev_out_mute);
971+
codec.close();
972+
s_playing = false;
973+
#endif
974+
}
975+
862976
void TLoRaPagerBoard::setHapticEffects(uint8_t effects)
863977
{
864978
if (effects > 127) effects = 127;

src/board/TLoRaPagerBoard.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,11 @@ class TLoRaPagerBoard : public BoardBase,
216216
*/
217217
void stopVibrator() override;
218218

219+
/**
220+
* @brief Play incoming-message prompt tone
221+
*/
222+
void playMessageTone() override;
223+
219224
/**
220225
* @brief Set haptic effect waveform
221226
* @param effects Effect number (0-127, see DRV2605 documentation)

variants/tdeck/pins_arduino.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ static const uint8_t SCK = 40;
2929
// Board power enable (required very early on T-Deck)
3030
#define BOARD_POWERON (10)
3131

32+
// T-Deck I2S speaker/amp pins (aligned with Meshtastic T-Deck reference).
33+
#define DAC_I2S_BCK (7)
34+
#define DAC_I2S_WS (5)
35+
#define DAC_I2S_DOUT (6)
36+
#define DAC_I2S_MCLK (21)
37+
3238
// Display pins (aligned with LilyGo T-Deck examples)
3339
#define DISP_MOSI (MOSI)
3440
#define DISP_MISO (MISO)

0 commit comments

Comments
 (0)