Files
pyxis/lib/lxst_audio/lxst_audio.cpp
torlando-tech 6744eb136d LXST voice call stability: fix hangup crash, signal queue, TX pump, mic tuning
- Fix use-after-free crash on hangup: set _call_state=IDLE before deleting
  _lxst_audio, preventing pump_call_tx() (runs without LVGL lock) from
  accessing freed memory
- Replace single-slot _call_signal_pending with 8-element ring buffer queue
  to prevent signal loss when CONNECTING+ESTABLISHED arrive in rapid succession
- Extract TX pump into pump_call_tx() called right after reticulum->loop()
  for low-latency audio TX without LVGL lock dependency (was buried at step 10)
- Tune ES7210 mic gain to 21dB (was 15dB) to improve Codec2 input level
  without ADC clipping that occurred at 24dB
- I2S capture: use APLL for accurate 8kHz clock, direct 8kHz sampling
  (no more 16→8kHz decimation), DMA 16x64 for encode burst headroom
- Reduce Reticulum log verbosity to LOG_INFO (was LOG_TRACE)
- BLE: add ble_hs_sched_reset() tiered recovery before reboot on desync,
  widen supervision timeout to 4.0s for WiFi coexistence
- Add UDP multicast log broadcasting and OTA flash support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 10:57:14 -05:00

356 lines
11 KiB
C++

// Copyright (c) 2024 LXST contributors
// SPDX-License-Identifier: MPL-2.0
#include "lxst_audio.h"
#ifdef ARDUINO
#include <esp_log.h>
#include <esp_system.h>
#include <Hardware/TDeck/Config.h>
#include <Arduino.h>
#include <driver/i2s.h>
#include "es7210.h"
#include "i2s_capture.h"
#include "i2s_playback.h"
#include "codec_wrapper.h"
#include "Tone.h"
using namespace Hardware::TDeck;
static const char* TAG = "LXST:Audio";
LXSTAudio::LXSTAudio() = default;
LXSTAudio::~LXSTAudio() {
deinit();
}
bool LXSTAudio::init(int codec2Mode, uint8_t micGain) {
if (initialized_) {
ESP_LOGW(TAG, "Already initialized");
return true;
}
codec2Mode_ = codec2Mode;
Serial.printf("[AUDIO] Init starting (heap=%lu)...\n", (unsigned long)esp_get_free_heap_size());
// Initialize ES7210 using LilyGO library — exact same calls as their Microphone example
// SLAVE mode: ES7210 receives MCLK/BCLK/LRCK from ESP32 I2S master
Serial.println("[AUDIO] Initializing ES7210 via LilyGO library...");
{
audio_hal_codec_config_t cfg = {};
cfg.adc_input = AUDIO_HAL_ADC_INPUT_ALL;
cfg.codec_mode = AUDIO_HAL_CODEC_MODE_ENCODE;
cfg.i2s_iface.mode = AUDIO_HAL_MODE_SLAVE;
cfg.i2s_iface.fmt = AUDIO_HAL_I2S_NORMAL;
cfg.i2s_iface.samples = AUDIO_HAL_08K_SAMPLES;
cfg.i2s_iface.bits = AUDIO_HAL_BIT_LENGTH_16BITS;
uint32_t ret_val = ESP_OK;
ret_val |= es7210_adc_init(&Wire, &cfg);
ret_val |= es7210_adc_config_i2s(cfg.codec_mode, &cfg.i2s_iface);
ret_val |= es7210_adc_set_gain(
(es7210_input_mics_t)(ES7210_INPUT_MIC1 | ES7210_INPUT_MIC2 |
ES7210_INPUT_MIC3 | ES7210_INPUT_MIC4),
(es7210_gain_value_t)micGain);
ret_val |= es7210_adc_ctrl_state(cfg.codec_mode, AUDIO_HAL_CTRL_START);
if (ret_val != ESP_OK) {
Serial.printf("[AUDIO] ES7210 init warning: ret=%lu\n", (unsigned long)ret_val);
}
}
// Verify ES7210 configuration by reading back key registers
{
int reg00 = es7210_read_reg(ES7210_RESET_REG00);
int reg01 = es7210_read_reg(ES7210_CLOCK_OFF_REG01);
int reg06 = es7210_read_reg(ES7210_POWER_DOWN_REG06);
int reg08 = es7210_read_reg(ES7210_MODE_CONFIG_REG08);
int reg43 = es7210_read_reg(ES7210_MIC1_GAIN_REG43);
int reg47 = es7210_read_reg(ES7210_MIC1_POWER_REG47);
int reg4b = es7210_read_reg(ES7210_MIC12_POWER_REG4B);
Serial.printf("[AUDIO] ES7210 regs: R00=0x%02X R01=0x%02X R06=0x%02X R08=0x%02X "
"GAIN1=0x%02X PWR1=0x%02X PWR12=0x%02X\n",
reg00, reg01, reg06, reg08, reg43, reg47, reg4b);
// Expected: R00=0x41 (normal), R06=0x00 (powered up), R08=0x00 (slave mode)
// PWR1=0x00, PWR12=0x00 (mics powered on)
if (reg06 != 0x00) {
Serial.printf("[AUDIO] WARNING: ES7210 POWER_DOWN=0x%02X (expected 0x00)\n", reg06);
}
if (reg47 != 0x00 || reg4b != 0x00) {
Serial.printf("[AUDIO] WARNING: ES7210 mic power not active! R47=0x%02X R4B=0x%02X\n", reg47, reg4b);
}
}
Serial.println("[AUDIO] ES7210 initialized OK");
// I2S capture init
capture_ = new I2SCapture();
if (!capture_->init()) {
Serial.println("[AUDIO] I2S capture init FAILED");
delete capture_;
capture_ = nullptr;
return false;
}
Serial.println("[AUDIO] I2S capture initialized (MCLK now running)");
// Re-issue ES7210 start with clocks now running.
// In slave mode, the ES7210 needs MCLK/BCLK/LRCK from the ESP32 I2S master
// to properly start its ADC — the initial start above ran before clocks were
// available. This second call ensures the ADC powers up correctly.
{
audio_hal_codec_config_t cfg2 = {};
cfg2.codec_mode = AUDIO_HAL_CODEC_MODE_ENCODE;
es7210_adc_ctrl_state(cfg2.codec_mode, AUDIO_HAL_CTRL_START);
Serial.println("[AUDIO] ES7210 re-started with I2S clocks active");
}
// Create separate Codec2 instances for encode and decode to avoid mutex
// contention during full-duplex calls (capture task + main thread decode)
encodeCodec_ = new Codec2Wrapper();
if (!encodeCodec_->create(codec2Mode)) {
Serial.println("[AUDIO] Codec2 encoder create FAILED");
delete capture_;
capture_ = nullptr;
delete encodeCodec_;
encodeCodec_ = nullptr;
return false;
}
Serial.printf("[AUDIO] Codec2 encoder created (heap=%lu)\n", (unsigned long)esp_get_free_heap_size());
decodeCodec_ = new Codec2Wrapper();
if (!decodeCodec_->create(codec2Mode)) {
Serial.println("[AUDIO] Codec2 decoder create FAILED");
delete capture_;
capture_ = nullptr;
encodeCodec_->destroy();
delete encodeCodec_;
encodeCodec_ = nullptr;
delete decodeCodec_;
decodeCodec_ = nullptr;
return false;
}
Serial.printf("[AUDIO] Codec2 decoder created (heap=%lu)\n", (unsigned long)esp_get_free_heap_size());
// Configure encoder on capture side (owns its codec instance)
if (!capture_->configureEncoder(encodeCodec_, true)) {
Serial.println("[AUDIO] Capture encoder config FAILED");
delete capture_;
capture_ = nullptr;
encodeCodec_->destroy();
delete encodeCodec_;
encodeCodec_ = nullptr;
decodeCodec_->destroy();
delete decodeCodec_;
decodeCodec_ = nullptr;
return false;
}
// Create playback engine with its own codec instance
playback_ = new I2SPlayback();
if (!playback_->configureDecoder(decodeCodec_)) {
ESP_LOGE(TAG, "Playback decoder config failed");
delete capture_;
capture_ = nullptr;
delete playback_;
playback_ = nullptr;
encodeCodec_->destroy();
delete encodeCodec_;
encodeCodec_ = nullptr;
decodeCodec_->destroy();
delete decodeCodec_;
decodeCodec_ = nullptr;
return false;
}
initialized_ = true;
state_ = State::IDLE;
ESP_LOGI(TAG, "LXST Audio initialized: Codec2 mode %d (heap=%lu)",
codec2Mode, (unsigned long)esp_get_free_heap_size());
return true;
}
void LXSTAudio::deinit() {
stopCapture();
stopPlayback();
if (capture_) {
capture_->releaseBuffers();
delete capture_;
capture_ = nullptr;
}
if (playback_) {
playback_->releaseBuffers();
delete playback_;
playback_ = nullptr;
}
if (encodeCodec_) {
encodeCodec_->destroy();
delete encodeCodec_;
encodeCodec_ = nullptr;
}
if (decodeCodec_) {
decodeCodec_->destroy();
delete decodeCodec_;
decodeCodec_ = nullptr;
}
initialized_ = false;
state_ = State::IDLE;
ESP_LOGI(TAG, "LXST Audio deinitialized");
}
bool LXSTAudio::startCapture() {
if (!initialized_ || !capture_) return false;
// Half-duplex: stop playback first
if (state_ == State::PLAYING) {
stopPlayback();
}
if (state_ == State::CAPTURING || state_ == State::FULL_DUPLEX) return true;
if (!capture_->start()) {
ESP_LOGE(TAG, "Failed to start capture");
return false;
}
state_ = State::CAPTURING;
ESP_LOGI(TAG, "Capture started (TX mode)");
return true;
}
void LXSTAudio::stopCapture() {
if (!capture_) return;
if (state_ != State::CAPTURING && state_ != State::FULL_DUPLEX) return;
capture_->stop();
// Re-init capture I2S for next use
capture_->init();
capture_->configureEncoder(encodeCodec_, true);
if (state_ == State::FULL_DUPLEX) {
state_ = State::PLAYING;
ESP_LOGI(TAG, "Capture stopped (still playing)");
} else {
state_ = State::IDLE;
ESP_LOGI(TAG, "Capture stopped");
}
}
bool LXSTAudio::startPlayback() {
if (!initialized_ || !playback_) return false;
// Half-duplex: stop capture first
if (state_ == State::CAPTURING) {
stopCapture();
}
if (state_ == State::PLAYING || state_ == State::FULL_DUPLEX) return true;
// Release I2S_NUM_0 from tone generator
Notification::tone_deinit();
if (!playback_->start()) {
ESP_LOGE(TAG, "Failed to start playback");
return false;
}
state_ = State::PLAYING;
ESP_LOGI(TAG, "Playback started (RX mode)");
return true;
}
void LXSTAudio::stopPlayback() {
if (!playback_) return;
if (state_ != State::PLAYING && state_ != State::FULL_DUPLEX) return;
playback_->stop();
// Re-configure decoder for next use
playback_->configureDecoder(decodeCodec_);
if (state_ == State::FULL_DUPLEX) {
state_ = State::CAPTURING;
ESP_LOGI(TAG, "Playback stopped (still capturing)");
} else {
state_ = State::IDLE;
ESP_LOGI(TAG, "Playback stopped");
}
// Tone.cpp will re-init its I2S on next tone_play() call
// (tone_init() checks _initialized flag)
}
bool LXSTAudio::startFullDuplex() {
if (!initialized_ || !capture_ || !playback_) return false;
if (state_ == State::FULL_DUPLEX) return true;
ESP_LOGI(TAG, "Starting full-duplex (heap=%lu)...", (unsigned long)esp_get_free_heap_size());
// Release I2S_NUM_0 from tone generator
Notification::tone_deinit();
// Start playback (speaker) first — I2S_NUM_0
if (!playback_->isPlaying()) {
ESP_LOGI(TAG, "Starting playback (heap=%lu)...", (unsigned long)esp_get_free_heap_size());
if (!playback_->start()) {
ESP_LOGE(TAG, "Failed to start playback for full-duplex");
return false;
}
ESP_LOGI(TAG, "Playback started (heap=%lu)", (unsigned long)esp_get_free_heap_size());
}
// Start capture (mic) — I2S_NUM_1
if (!capture_->isCapturing()) {
ESP_LOGI(TAG, "Starting capture (heap=%lu)...", (unsigned long)esp_get_free_heap_size());
if (!capture_->start()) {
ESP_LOGE(TAG, "Failed to start capture for full-duplex");
playback_->stop();
playback_->configureDecoder(decodeCodec_);
return false;
}
ESP_LOGI(TAG, "Capture started (heap=%lu)", (unsigned long)esp_get_free_heap_size());
}
state_ = State::FULL_DUPLEX;
ESP_LOGI(TAG, "Full-duplex started (TX+RX, heap=%lu)", (unsigned long)esp_get_free_heap_size());
return true;
}
bool LXSTAudio::readEncodedPacket(uint8_t* dest, int maxLength, int* actualLength) {
if (!capture_ || !isCapturing()) return false;
return capture_->readEncodedPacket(dest, maxLength, actualLength);
}
bool LXSTAudio::writeEncodedPacket(const uint8_t* data, int length) {
if (!playback_ || !isPlaying()) return false;
return playback_->writeEncodedPacket(data, length);
}
void LXSTAudio::setCaptureMute(bool muted) {
if (capture_) capture_->setMute(muted);
}
void LXSTAudio::setPlaybackMute(bool muted) {
if (playback_) playback_->setMute(muted);
}
int LXSTAudio::capturePacketsAvailable() const {
if (!capture_) return 0;
return capture_->availablePackets();
}
int LXSTAudio::playbackFramesBuffered() const {
if (!playback_) return 0;
return playback_->bufferedFrames();
}
#endif // ARDUINO