Files
pyxis/lib/lxst_audio/i2s_capture.h
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

116 lines
3.8 KiB
C++

// Copyright (c) 2024 LXST contributors
// SPDX-License-Identifier: MPL-2.0
#pragma once
#include <cstdint>
#include <atomic>
class EncodedRingBuffer;
class VoiceFilterChain;
class Codec2Wrapper;
/**
* ESP32 I2S microphone capture engine for LXST voice streaming.
*
* Uses I2S_NUM_1 to capture audio from the ES7210 mic array.
* Runs a FreeRTOS task that reads I2S DMA, applies voice filters,
* encodes with Codec2, and writes to an EncodedRingBuffer for
* the network layer to consume.
*
* Audio flow:
* I2S DMA -> accumulate to frame -> filter -> encode -> EncodedRingBuffer
*/
class I2SCapture {
public:
I2SCapture();
~I2SCapture();
I2SCapture(const I2SCapture&) = delete;
I2SCapture& operator=(const I2SCapture&) = delete;
/**
* Initialize the I2S capture port.
* Does NOT start capturing — call start() after init.
* @return true on success
*/
bool init();
/**
* Configure the encoder. Must be called before start().
* @param codec Shared Codec2Wrapper (not owned — caller manages lifecycle)
* @param enableFilters Whether to apply HPF+LPF+AGC filter chain
* @return true on success
*/
bool configureEncoder(Codec2Wrapper* codec, bool enableFilters = true);
/** Start the capture task. Returns immediately. */
bool start();
/** Stop the capture task and release I2S resources. */
void stop();
/** Mute/unmute the microphone (sends silence when muted). */
void setMute(bool muted) { muted_.store(muted, std::memory_order_relaxed); }
bool isMuted() const { return muted_.load(std::memory_order_relaxed); }
/** Check if currently capturing. */
bool isCapturing() const { return capturing_.load(std::memory_order_relaxed); }
/**
* Read the next encoded packet from the ring buffer.
* Called by the network layer.
*
* @param dest Output buffer for encoded packet
* @param maxLength Size of output buffer
* @param actualLength [out] Actual packet size
* @return true if a packet was read
*/
bool readEncodedPacket(uint8_t* dest, int maxLength, int* actualLength);
/** Number of encoded packets waiting in the ring buffer. */
int availablePackets() const;
/** Release capture buffers (does NOT destroy the shared codec). */
void releaseBuffers();
private:
static void captureTask(void* param);
void captureLoop();
bool i2sInitialized_ = false;
std::atomic<bool> capturing_{false};
std::atomic<bool> muted_{false};
void* taskHandle_ = nullptr;
// Audio pipeline components
Codec2Wrapper* codec_ = nullptr; // Shared, not owned
VoiceFilterChain* filterChain_ = nullptr;
EncodedRingBuffer* encodedRing_ = nullptr;
// Accumulation buffer: I2S delivers variable bursts, we need fixed-size frames
int16_t* accumBuffer_ = nullptr;
int accumCount_ = 0;
int frameSamples_ = 0; // Codec2 samples per frame (e.g., 320 for 700C, 160 for 1600/3200)
// Pre-allocated encode output buffer
uint8_t encodeBuf_[256];
// Silence buffer for mute
int16_t* silenceBuf_ = nullptr;
bool filtersEnabled_ = true;
static constexpr int I2S_SAMPLE_RATE = 8000; // I2S runs at 8kHz — matches Codec2 directly, no resampling needed
static constexpr int CODEC_SAMPLE_RATE = 8000; // Codec2 expects 8kHz
// Accumulate this many codec frames before filter+encode.
// Matches Columba's 200ms batch (1600 samples for Codec2 3200).
// The AGC needs large blocks for stable gain tracking.
static constexpr int FRAMES_PER_BATCH = 10;
static constexpr int ENCODED_RING_SLOTS = 128;
static constexpr int ENCODED_RING_MAX_BYTES = 256;
static constexpr int CAPTURE_TASK_STACK = 24576; // 24KB — pyxis_log→sendto uses ~4KB lwIP stack
static constexpr int CAPTURE_TASK_PRIORITY = 5;
static constexpr int CAPTURE_TASK_CORE = 0;
};