fix(pager): restore voice playback gain

This commit is contained in:
liu weikai
2026-08-12 10:07:06 +08:00
parent 86f1da1923
commit a9df6ea297
5 changed files with 169 additions and 12 deletions
@@ -59,6 +59,9 @@ int main(int argc, char** argv)
"voice/vmp_pager_session.h");
const std::string pager_audio = readFile(
root / "platform/esp/arduino_common/src/voice/vmp_pager_audio.cpp");
const std::string pager_audio_header = readFile(
root / "platform/esp/arduino_common/include/platform/esp/arduino_common/"
"voice/vmp_pager_audio.h");
const std::string codec2 = readFile(root / "third_party/codec2/src/codec2.c");
const std::string codec2_internal =
readFile(root / "third_party/codec2/src/codec2_internal.h");
@@ -324,6 +327,46 @@ int main(int argc, char** argv)
assert(session.find("stack_budget_bytes=%u") != std::string::npos);
assert(session.find("logTaskCreateFailure(\"vmp_tx\", kOutboundTaskStackBytes)") !=
std::string::npos);
// Voice-message playback is speech, not a notification tone. Its former
// hard-coded 70% value maps to -15 dB in the ES8311 default volume curve.
// Keep it at full output gain unless a future dedicated voice-volume
// setting supplies an explicit user value.
assert(session.find("playback_codec_,\n 100U)") !=
std::string::npos);
assert(pager_header.find("volume_percent = 100U") != std::string::npos);
assert(pager_audio_header.find("volume_percent = 100U") !=
std::string::npos);
const std::size_t capture_audio =
positionOf(pager_audio, "bool beginCaptureAudio(");
const std::size_t playback_audio =
positionOfAfter(pager_audio, "bool beginPlaybackAudio(", capture_audio);
const std::size_t pcm_level =
positionOfAfter(pager_audio, "struct PcmLevel", playback_audio);
const std::string capture_audio_body =
pager_audio.substr(capture_audio, playback_audio - capture_audio);
const std::string playback_audio_body =
pager_audio.substr(playback_audio, pcm_level - playback_audio);
assert(capture_audio_body.find("audioSetGain(kOwner, kCaptureGainDb)") !=
std::string::npos);
assert(capture_audio_body.find("audioSetMute(kOwner, false)") !=
std::string::npos);
assert(capture_audio_body.find("audioSetOutMute") == std::string::npos);
assert(playback_audio_body.find("audioSetOutMute(kOwner, false)") !=
std::string::npos);
assert(playback_audio_body.find("audioSetGain") == std::string::npos);
assert(playback_audio_body.find("audioSetMute") == std::string::npos);
// Peak/RMS inspection must run on the existing PCM frame. It must not add
// a retained frame, queue, or other RAM-backed telemetry buffer.
assert(pager_audio.find("kAudioLevelLogIntervalFrames = 25U") !=
std::string::npos);
assert(pager_audio.find("[VMP][AUDIO] level capture") !=
std::string::npos);
assert(pager_audio.find("[VMP][AUDIO] level playback") !=
std::string::npos);
assert(pager_audio.find("struct PagerCodec2Audio::FrameScratch\n{\n int16_t stereo[") !=
std::string::npos);
assert(pager_audio.find("PcmLevel stereo[") == std::string::npos);
assert(pager_audio.find("PcmLevel mono[") == std::string::npos);
assert(codec2_internal.find("CODEC2_1300_SCRATCH") != std::string::npos);
assert(codec2_internal.find("COMP *fft_inplace_scratch") !=
std::string::npos);
@@ -80,7 +80,7 @@ class PagerCodec2Audio final
PlaybackResult play(const uint8_t* encoded_media,
std::size_t encoded_media_len,
chat::voice::vmp::Codec codec,
uint8_t volume_percent = 70U);
uint8_t volume_percent = 100U);
private:
struct FrameScratch;
@@ -110,7 +110,7 @@ std::size_t listInboxMetadata(chat::voice::vmp::VoiceMessageMetadata* out_metada
std::size_t capacity);
/** @brief Decodes one already-local VMP inbox object to the Pager speaker. */
bool playInboxMessage(uint64_t local_id, uint8_t volume_percent = 70U);
bool playInboxMessage(uint64_t local_id, uint8_t volume_percent = 100U);
/**
* @brief Starts local playback on a VMP worker so the UI task never blocks.
@@ -5,6 +5,7 @@
#include "platform/esp/arduino_common/voice/vmp_pager_audio.h"
#include <cmath>
#include <cstring>
#include <new>
@@ -27,6 +28,7 @@ using ::boards::tlora_pager::TLoRaPagerBoard;
constexpr PagerAudioOwner kOwner = PagerAudioOwner::VoiceMessage;
constexpr float kCaptureGainDb = 24.0F;
constexpr std::size_t kAudioLevelLogIntervalFrames = 25U;
TLoRaPagerBoard* pagerBoard()
{
@@ -61,14 +63,14 @@ int16_t clampToInt16(int32_t value)
return static_cast<int16_t>(value);
}
bool beginAudio(TLoRaPagerBoard* board, bool speaker_enabled)
bool beginCaptureAudio(TLoRaPagerBoard* board)
{
if (!board ||
board->openAudioSession(kOwner,
kBitsPerSample,
kHardwareChannels,
kSampleRateHz,
speaker_enabled) != 0)
false) != 0)
{
return false;
}
@@ -78,7 +80,21 @@ bool beginAudio(TLoRaPagerBoard* board, bool speaker_enabled)
board->closeAudioSession(kOwner);
return false;
}
if (speaker_enabled && !board->audioSetOutMute(kOwner, false))
return true;
}
bool beginPlaybackAudio(TLoRaPagerBoard* board)
{
if (!board ||
board->openAudioSession(kOwner,
kBitsPerSample,
kHardwareChannels,
kSampleRateHz,
true) != 0)
{
return false;
}
if (!board->audioSetOutMute(kOwner, false))
{
board->closeAudioSession(kOwner);
return false;
@@ -86,6 +102,83 @@ bool beginAudio(TLoRaPagerBoard* board, bool speaker_enabled)
return true;
}
struct PcmLevel
{
uint16_t peak = 0U;
uint16_t rms = 0U;
};
struct CapturePcmLevel
{
PcmLevel left{};
PcmLevel right{};
PcmLevel mono{};
};
PcmLevel measurePcmLevel(const int16_t* samples,
std::size_t sample_count,
std::size_t stride = 1U,
std::size_t offset = 0U)
{
uint32_t peak = 0U;
uint64_t sum_of_squares = 0U;
for (std::size_t index = 0U; index < sample_count; ++index)
{
const int32_t sample = samples[index * stride + offset];
const uint32_t magnitude = sample < 0 ? static_cast<uint32_t>(-sample)
: static_cast<uint32_t>(sample);
if (magnitude > peak)
{
peak = magnitude;
}
sum_of_squares += static_cast<uint64_t>(sample * sample);
}
const float mean_square =
static_cast<float>(sum_of_squares) / static_cast<float>(sample_count);
return {static_cast<uint16_t>(peak),
static_cast<uint16_t>(sqrtf(mean_square) + 0.5F)};
}
CapturePcmLevel measureCapturePcmLevel(const int16_t* stereo, const int16_t* mono)
{
return {measurePcmLevel(stereo, kCodec2SamplesPerFrame, kHardwareChannels, 0U),
measurePcmLevel(stereo, kCodec2SamplesPerFrame, kHardwareChannels, 1U),
measurePcmLevel(mono, kCodec2SamplesPerFrame)};
}
bool shouldLogPcmLevel(std::size_t frame_index, std::size_t frame_count)
{
const std::size_t completed_frames = frame_index + 1U;
return frame_index == 0U ||
(completed_frames % kAudioLevelLogIntervalFrames) == 0U ||
completed_frames == frame_count;
}
void logCapturePcmLevel(std::size_t frame_index, const CapturePcmLevel& level)
{
Serial.printf("[VMP][AUDIO] level capture frame=%u left_peak=%u left_rms=%u "
"right_peak=%u right_rms=%u mono_peak=%u mono_rms=%u\n",
static_cast<unsigned>(frame_index + 1U),
static_cast<unsigned>(level.left.peak),
static_cast<unsigned>(level.left.rms),
static_cast<unsigned>(level.right.peak),
static_cast<unsigned>(level.right.rms),
static_cast<unsigned>(level.mono.peak),
static_cast<unsigned>(level.mono.rms));
}
void logPlaybackPcmLevel(std::size_t frame_index,
const PcmLevel& level,
uint8_t output_volume)
{
Serial.printf("[VMP][AUDIO] level playback frame=%u mono_peak=%u mono_rms=%u "
"output_volume=%u\n",
static_cast<unsigned>(frame_index + 1U),
static_cast<unsigned>(level.peak),
static_cast<unsigned>(level.rms),
static_cast<unsigned>(output_volume));
}
} // namespace
struct PagerCodec2Audio::FrameScratch
@@ -111,7 +204,7 @@ CaptureResult PagerCodec2Audio::capture(const volatile bool* stop_requested)
Serial.printf("[VMP][AUDIO] capture rejected reason=unsupported\n");
return CaptureResult::Unsupported;
}
if (!beginAudio(board, false))
if (!beginCaptureAudio(board))
{
releaseFrameScratch();
Serial.printf("[VMP][AUDIO] capture rejected reason=audio_busy\n");
@@ -135,6 +228,8 @@ CaptureResult PagerCodec2Audio::capture(const volatile bool* stop_requested)
}
CaptureResult result = CaptureResult::Complete;
CapturePcmLevel last_capture_level{};
bool has_capture_level = false;
bool stopped_by_release = false;
for (std::size_t frame = 0; frame < kCodec2FramesPerMessage; ++frame)
{
@@ -154,6 +249,13 @@ CaptureResult PagerCodec2Audio::capture(const volatile bool* stop_requested)
break;
}
mixCaptureToMono();
last_capture_level = measureCapturePcmLevel(frame_scratch_->stereo,
frame_scratch_->mono);
has_capture_level = true;
if (shouldLogPcmLevel(frame, kCodec2FramesPerMessage))
{
logCapturePcmLevel(frame, last_capture_level);
}
codec2_encode(encoder,
encoded_media_ + encoded_media_size_,
frame_scratch_->mono);
@@ -169,6 +271,11 @@ CaptureResult PagerCodec2Audio::capture(const volatile bool* stop_requested)
codec2_destroy(encoder);
board->closeAudioSession(kOwner);
const std::size_t captured_frame_count = encoded_media_size_ / kCodec2BytesPerFrame;
if (has_capture_level && captured_frame_count % kAudioLevelLogIntervalFrames != 0U)
{
logCapturePcmLevel(captured_frame_count - 1U, last_capture_level);
}
releaseFrameScratch();
const std::size_t encoded_size = encoded_media_size_;
if (result != CaptureResult::Complete)
@@ -223,13 +330,13 @@ PlaybackResult PagerCodec2Audio::play(const uint8_t* encoded_media,
{
return PlaybackResult::Unsupported;
}
if (!beginAudio(board, true))
if (!beginPlaybackAudio(board))
{
releaseFrameScratch();
return PlaybackResult::AudioBusy;
}
(void)board->audioSetVolume(kOwner, volume_percent > 100U ? 100U
: volume_percent);
const uint8_t output_volume = volume_percent > 100U ? 100U : volume_percent;
(void)board->audioSetVolume(kOwner, output_volume);
CODEC2* const decoder = codec2_create(CODEC2_MODE_1300);
const int sample_count = decoder ? codec2_samples_per_frame(decoder) : 0;
@@ -248,12 +355,19 @@ PlaybackResult PagerCodec2Audio::play(const uint8_t* encoded_media,
codec2_set_lpc_post_filter(decoder, 1, 0, 0.8F, 0.2F);
PlaybackResult result = PlaybackResult::Complete;
for (std::size_t offset = 0U; offset < encoded_media_len;
offset += kCodec2BytesPerFrame)
const std::size_t frame_count = encoded_media_len / kCodec2BytesPerFrame;
for (std::size_t frame_index = 0U, offset = 0U; offset < encoded_media_len;
++frame_index, offset += kCodec2BytesPerFrame)
{
codec2_decode(decoder,
frame_scratch_->mono,
const_cast<uint8_t*>(encoded_media + offset));
const PcmLevel level =
measurePcmLevel(frame_scratch_->mono, kCodec2SamplesPerFrame);
if (shouldLogPcmLevel(frame_index, frame_count))
{
logPlaybackPcmLevel(frame_index, level, output_volume);
}
duplicatePlaybackToStereo();
if (!writePlaybackFrame())
{
@@ -1038,7 +1038,7 @@ class PagerReceiveSession final
result = media_->audio.play(media_->playback_media,
playback_media_len_,
playback_codec_,
70U);
100U);
}
logCurrentTaskStack("vmp_play", "after_playback");
Serial.printf("[VMP][PLAY] end local_id=%llu result=%u\n",