mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-09-01 16:48:23 +00:00
Fix LXST TX audio wire format to match Columba's expected batch size
Columba's native OboePlaybackEngine ring buffer expects exactly frameSamples (1600 for Codec2 3200 mode) decoded samples per writeEncodedPacket call = 10 sub-frames of 160 samples each. Changes: - Batch exactly 10 sub-frames per fixarray element (82 bytes each: codec_type + mode_header + 10*8 raw bytes) - Up to 2 batches per msgpack packet, matching Columba C2C format - Proper fixarray wrapping for multi-batch, bare bin8 for single - Add codec_type byte (0x02) prefix per batch element - Respond to PREFERRED_PROFILE negotiation with LBW (Codec2 3200) - Add capture diagnostics (raw PCM peaks, I2S dump, rate logging) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
6e47cb808b
commit
ddd19a04db
@@ -18,6 +18,9 @@ using namespace Hardware::TDeck;
|
||||
|
||||
static const char* TAG = "LXST:Capture";
|
||||
|
||||
// Defined in main.cpp — sends to both Serial and UDP
|
||||
extern "C" void pyxis_log(const char* msg);
|
||||
|
||||
I2SCapture::I2SCapture() = default;
|
||||
|
||||
I2SCapture::~I2SCapture() {
|
||||
@@ -211,13 +214,18 @@ void I2SCapture::captureLoop() {
|
||||
|
||||
static constexpr int16_t LIMITER_THRESHOLD = 16000;
|
||||
|
||||
Serial.printf("[CAP] Capture task on core %d, I2S=%dHz, codec=%dHz, FIR=%d-tap\n",
|
||||
xPortGetCoreID(), I2S_SAMPLE_RATE, CODEC_SAMPLE_RATE, FIR_TAPS);
|
||||
{
|
||||
char logbuf[96];
|
||||
snprintf(logbuf, sizeof(logbuf), "[CAP] Capture task on core %d, I2S=%dHz, codec=%dHz, FIR=%d-tap, stack=%d",
|
||||
xPortGetCoreID(), I2S_SAMPLE_RATE, CODEC_SAMPLE_RATE, FIR_TAPS, CAPTURE_TASK_STACK);
|
||||
pyxis_log(logbuf);
|
||||
}
|
||||
uint32_t framesEncoded = 0;
|
||||
uint32_t totalDsSamples = 0; // Total mono samples after decimation
|
||||
uint32_t rateCheckMs = millis(); // For sample rate measurement
|
||||
int16_t runningPeakDs = 0; // Peak of decimated samples per interval
|
||||
int16_t runningPeakRaw = 0; // Peak of raw I2S samples per interval
|
||||
uint32_t ringDrops = 0; // Ring buffer overflow counter
|
||||
|
||||
while (capturing_.load(std::memory_order_relaxed)) {
|
||||
// Read samples from I2S DMA (at 16kHz)
|
||||
@@ -227,15 +235,15 @@ void I2SCapture::captureLoop() {
|
||||
|
||||
int samplesRead = bytesRead / sizeof(int16_t);
|
||||
|
||||
// One-time dump of first raw I2S samples to see TDM channel layout
|
||||
if (framesEncoded == 0 && samplesRead >= 16) {
|
||||
static bool dumped = false;
|
||||
if (!dumped) {
|
||||
dumped = true;
|
||||
Serial.printf("[CAP] Raw I2S first 16 samples (%d total): ", samplesRead);
|
||||
for (int d = 0; d < 16; d++) Serial.printf("%d ", readBuf[d]);
|
||||
Serial.println();
|
||||
}
|
||||
// Dump first raw I2S samples on each capture start to see TDM channel layout
|
||||
// Resets per capture start (not per boot) since framesEncoded resets to 0
|
||||
if (framesEncoded == 0 && samplesRead >= 16 && totalDsSamples == 0) {
|
||||
char rawdump[192];
|
||||
int pos = snprintf(rawdump, sizeof(rawdump),
|
||||
"[CAP] Raw I2S (%d read, %zu bytes): ", samplesRead, bytesRead);
|
||||
for (int d = 0; d < 16 && pos < 180; d++)
|
||||
pos += snprintf(rawdump + pos, sizeof(rawdump) - pos, "%d ", readBuf[d]);
|
||||
pyxis_log(rawdump);
|
||||
}
|
||||
|
||||
// Track raw I2S peak (all channels)
|
||||
@@ -291,9 +299,13 @@ void I2SCapture::captureLoop() {
|
||||
uint32_t elapsed = now - rateCheckMs;
|
||||
if (elapsed >= 2000) {
|
||||
uint32_t rate = (totalDsSamples * 1000) / elapsed;
|
||||
Serial.printf("[CAP] rate=%luHz frames=%lu rawPeak=%d dsPeak=%d\n",
|
||||
(unsigned long)rate, (unsigned long)framesEncoded,
|
||||
runningPeakRaw, runningPeakDs);
|
||||
{
|
||||
char logbuf[128];
|
||||
snprintf(logbuf, sizeof(logbuf), "[CAP] rate=%luHz frames=%lu rawPeak=%d dsPeak=%d ringDrops=%lu",
|
||||
(unsigned long)rate, (unsigned long)framesEncoded,
|
||||
runningPeakRaw, runningPeakDs, (unsigned long)ringDrops);
|
||||
pyxis_log(logbuf);
|
||||
}
|
||||
totalDsSamples = 0;
|
||||
rateCheckMs = now;
|
||||
runningPeakRaw = 0;
|
||||
@@ -321,16 +333,18 @@ void I2SCapture::captureLoop() {
|
||||
filterChain_->process(frameData, frameSamples_, CODEC_SAMPLE_RATE);
|
||||
}
|
||||
|
||||
// Log PCM levels for first few frames (pre-filter)
|
||||
// Log PCM levels for first few frames and periodically
|
||||
if (framesEncoded < 5 || (framesEncoded % 500 == 0)) {
|
||||
int16_t maxVal = 0;
|
||||
for (int s = 0; s < frameSamples_; s++) {
|
||||
int16_t v = accumBuffer_[s] < 0 ? -accumBuffer_[s] : accumBuffer_[s];
|
||||
if (v > maxVal) maxVal = v;
|
||||
}
|
||||
Serial.printf("[CAP] PCM peak=%d (first=%d,%d,%d,%d)\n",
|
||||
maxVal, accumBuffer_[0], accumBuffer_[1],
|
||||
accumBuffer_[2], accumBuffer_[3]);
|
||||
char logbuf[96];
|
||||
snprintf(logbuf, sizeof(logbuf), "[CAP] PCM peak=%d (first=%d,%d,%d,%d)",
|
||||
maxVal, accumBuffer_[0], accumBuffer_[1],
|
||||
accumBuffer_[2], accumBuffer_[3]);
|
||||
pyxis_log(logbuf);
|
||||
}
|
||||
|
||||
// Encode
|
||||
@@ -339,19 +353,19 @@ void I2SCapture::captureLoop() {
|
||||
if (encodedLen > 0) {
|
||||
framesEncoded++;
|
||||
if (framesEncoded <= 3 || (framesEncoded % 500 == 0)) {
|
||||
char logbuf[128];
|
||||
char hex[64];
|
||||
int hpos = 0;
|
||||
for (int h = 0; h < encodedLen && h < 20 && hpos < 60; h++)
|
||||
hpos += snprintf(hex + hpos, 64 - hpos, "%02X ", encodeBuf_[h]);
|
||||
Serial.printf("[CAP] Encoded #%lu: %d bytes: %s\n",
|
||||
(unsigned long)framesEncoded, encodedLen, hex);
|
||||
snprintf(logbuf, sizeof(logbuf), "[CAP] Encoded #%lu: %d bytes: %s",
|
||||
(unsigned long)framesEncoded, encodedLen, hex);
|
||||
pyxis_log(logbuf);
|
||||
}
|
||||
}
|
||||
if (encodedLen > 0 && encodedRing_) {
|
||||
if (!encodedRing_->write(encodeBuf_, encodedLen)) {
|
||||
// Ring full — drop this frame (TX pump will drain)
|
||||
// NOTE: Do NOT call read() here — this is SPSC and
|
||||
// the TX pump is the sole consumer on another core.
|
||||
ringDrops++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,9 +103,9 @@ private:
|
||||
|
||||
static constexpr int I2S_SAMPLE_RATE = 16000; // I2S runs at 16kHz (matches T-Deck Plus reference)
|
||||
static constexpr int CODEC_SAMPLE_RATE = 8000; // Codec2 expects 8kHz — we downsample 2:1
|
||||
static constexpr int ENCODED_RING_SLOTS = 32;
|
||||
static constexpr int ENCODED_RING_SLOTS = 128;
|
||||
static constexpr int ENCODED_RING_MAX_BYTES = 256;
|
||||
static constexpr int CAPTURE_TASK_STACK = 16384;
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -60,6 +60,27 @@ bool LXSTAudio::init(int codec2Mode, uint8_t micGain) {
|
||||
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
|
||||
@@ -72,6 +93,17 @@ bool LXSTAudio::init(int codec2Mode, uint8_t micGain) {
|
||||
}
|
||||
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();
|
||||
|
||||
@@ -899,7 +899,8 @@ void UIManager::call_send_signal(int signal) {
|
||||
DEBUG(buf);
|
||||
}
|
||||
|
||||
void UIManager::call_send_audio_batch(const uint8_t* batch_data, int batch_len, int frame_count) {
|
||||
void UIManager::call_send_audio_batch(const uint8_t* batch_data, int batch_len,
|
||||
int batch_count, int total_frames) {
|
||||
if (!_call_link || _call_link.status() != Type::Link::ACTIVE) {
|
||||
if (_call_audio_tx_count == 0) {
|
||||
char dbg[64];
|
||||
@@ -910,60 +911,81 @@ void UIManager::call_send_audio_batch(const uint8_t* batch_data, int batch_len,
|
||||
return;
|
||||
}
|
||||
|
||||
// Msgpack: {0x01: bin8(codec_header + mode_header + raw_frames...)}
|
||||
// batch_data = [mode_header] + [raw_frame1] + [raw_frame2] + ... (headers stripped)
|
||||
uint8_t packet_buf[256];
|
||||
int total_len = 1 + batch_len; // codec_header + batch_data
|
||||
if (total_len > 250 || total_len < 1) return;
|
||||
// Match LXST-kt (Columba) wire format exactly:
|
||||
// {0x01: bin8(batch)} for single batch, or
|
||||
// {0x01: fixarray(N)[bin8(b1), bin8(b2), ...]} for multiple batches.
|
||||
// Each batch = [codec_type(0x02)] + [mode_header] + [10 * raw_codec2].
|
||||
// Columba's native ring buffer expects exactly frameSamples (1600) decoded
|
||||
// samples per writeEncodedPacket call. For Codec2 3200: 10 * 160 = 1600.
|
||||
// batch_data contains batch_count concatenated batches of 82 bytes each.
|
||||
static constexpr int BATCH_BYTES = 82; // codec_type(1) + mode(1) + 10*8
|
||||
|
||||
packet_buf[0] = 0x81; // fixmap(1)
|
||||
packet_buf[1] = 0x01; // key: FIELD_FRAMES
|
||||
packet_buf[2] = 0xC4; // bin8
|
||||
packet_buf[3] = (uint8_t)total_len; // length
|
||||
packet_buf[4] = LXST_CODEC_CODEC2; // codec header (0x02)
|
||||
memcpy(packet_buf + 5, batch_data, batch_len);
|
||||
uint8_t packet_buf[256];
|
||||
int pos = 0;
|
||||
|
||||
packet_buf[pos++] = 0x81; // fixmap(1)
|
||||
packet_buf[pos++] = 0x01; // key: FIELD_FRAMES
|
||||
|
||||
if (batch_count == 1) {
|
||||
// Single batch: bare bin8
|
||||
packet_buf[pos++] = 0xC4; // bin8
|
||||
packet_buf[pos++] = (uint8_t)BATCH_BYTES;
|
||||
memcpy(packet_buf + pos, batch_data, BATCH_BYTES);
|
||||
pos += BATCH_BYTES;
|
||||
} else {
|
||||
// Multiple batches: fixarray(N) of bin8 entries
|
||||
packet_buf[pos++] = 0x90 | (uint8_t)batch_count; // fixarray(N), N≤15
|
||||
for (int b = 0; b < batch_count; b++) {
|
||||
packet_buf[pos++] = 0xC4; // bin8
|
||||
packet_buf[pos++] = (uint8_t)BATCH_BYTES;
|
||||
memcpy(packet_buf + pos, batch_data + b * BATCH_BYTES, BATCH_BYTES);
|
||||
pos += BATCH_BYTES;
|
||||
}
|
||||
}
|
||||
|
||||
// Hex dump first TX packet for wire format verification
|
||||
if (_call_audio_tx_count < 2) {
|
||||
int pkt_len = 5 + batch_len;
|
||||
char hex[128];
|
||||
int pos = 0;
|
||||
for (int i = 0; i < pkt_len && i < 20 && pos < 120; i++) {
|
||||
pos += snprintf(hex + pos, 128 - pos, "%02X ", packet_buf[i]);
|
||||
int hpos = 0;
|
||||
for (int i = 0; i < pos && i < 24 && hpos < 120; i++) {
|
||||
hpos += snprintf(hex + hpos, 128 - hpos, "%02X ", packet_buf[i]);
|
||||
}
|
||||
char dbg[196];
|
||||
snprintf(dbg, sizeof(dbg), "LXST: TX wire[%d] %d frames: %s", pkt_len, frame_count, hex);
|
||||
snprintf(dbg, sizeof(dbg), "LXST: TX wire[%d] %d batches %d frames: %s",
|
||||
pos, batch_count, total_frames, hex);
|
||||
INFO(dbg);
|
||||
}
|
||||
|
||||
Bytes audio_data(packet_buf, 5 + batch_len);
|
||||
Bytes audio_data(packet_buf, pos);
|
||||
Packet packet(_call_link, audio_data);
|
||||
packet.send();
|
||||
}
|
||||
|
||||
void UIManager::call_rx_audio_frame(const uint8_t* frame, size_t frame_len) {
|
||||
// frame = [codec_header_byte, frame_data...]
|
||||
uint8_t codec = frame[0];
|
||||
const uint8_t* frame_data = frame + 1;
|
||||
size_t frame_data_len = frame_len - 1;
|
||||
// Wire format: [codec_type_byte] + [mode_header + codec2_subframes...]
|
||||
// codec_type: 0x00=Raw, 0x01=Opus, 0x02=Codec2 (matches LXST Codecs/__init__.py)
|
||||
// For Codec2: mode_header (0x00-0x06) + raw sub-frames
|
||||
uint8_t codec_type = frame[0];
|
||||
const uint8_t* codec_data = frame + 1;
|
||||
size_t codec_data_len = frame_len - 1;
|
||||
|
||||
if (codec != LXST_CODEC_CODEC2) {
|
||||
if (codec_type != LXST_CODEC_CODEC2) {
|
||||
if (_call_audio_rx_count == 0) {
|
||||
char dbg[64];
|
||||
snprintf(dbg, sizeof(dbg), "LXST: RX audio codec=0x%02X (expected 0x%02X), len=%d",
|
||||
codec, LXST_CODEC_CODEC2, (int)frame_data_len);
|
||||
snprintf(dbg, sizeof(dbg), "LXST: RX codec=0x%02X (need 0x02=Codec2), dropping",
|
||||
codec_type);
|
||||
WARNING(dbg);
|
||||
}
|
||||
return; // Can't decode Opus (0x01) or Raw (0x00) — only Codec2
|
||||
}
|
||||
|
||||
if (_lxst_audio && _lxst_audio->isPlaying()) {
|
||||
_lxst_audio->writeEncodedPacket(frame_data, frame_data_len);
|
||||
_lxst_audio->writeEncodedPacket(codec_data, codec_data_len);
|
||||
_call_audio_rx_count++;
|
||||
if (_call_audio_rx_count <= 3) {
|
||||
char dbg[80];
|
||||
snprintf(dbg, sizeof(dbg), "LXST: RX audio #%lu codec=0x%02X len=%d first=0x%02X",
|
||||
(unsigned long)_call_audio_rx_count, codec,
|
||||
(int)frame_data_len, frame_data_len > 0 ? frame_data[0] : 0);
|
||||
snprintf(dbg, sizeof(dbg), "LXST: RX audio #%lu mode=0x%02X len=%d",
|
||||
(unsigned long)_call_audio_rx_count, codec_data[0], (int)codec_data_len);
|
||||
INFO(dbg);
|
||||
}
|
||||
} else if (_call_audio_rx_count == 0) {
|
||||
@@ -1021,11 +1043,17 @@ void UIManager::call_on_packet(const Bytes& data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore PREFERRED_PROFILE signals (0xFF+) — profile negotiation not supported
|
||||
if (signal >= 0xFF) {
|
||||
char dbg[48];
|
||||
snprintf(dbg, sizeof(dbg), "LXST: Ignoring profile signal 0x%03X", signal);
|
||||
DEBUG(dbg);
|
||||
// Handle PREFERRED_PROFILE signals (0xFF+)
|
||||
// Remote sends PREFERRED_PROFILE + profile_id to request a codec profile.
|
||||
// Pyxis only supports Codec2, so respond with LBW (Codec2 3200bps).
|
||||
if (signal >= LXST_PREFERRED_PROFILE) {
|
||||
int remote_profile = signal - LXST_PREFERRED_PROFILE;
|
||||
char dbg[64];
|
||||
snprintf(dbg, sizeof(dbg), "LXST: Remote prefers profile 0x%02X, responding LBW (Codec2)",
|
||||
remote_profile);
|
||||
INFO(dbg);
|
||||
// Send our preferred profile (LBW = Codec2 3200bps)
|
||||
call_send_signal(LXST_PREFERRED_PROFILE + LXST_PROFILE_LBW);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1118,11 +1146,11 @@ void UIManager::call_process_signal(uint8_t signal) {
|
||||
case CallState::WAIT_RINGING:
|
||||
if (signal == LXST_STATUS_RINGING) {
|
||||
INFO("LXST: Remote is ringing");
|
||||
// Tell remote we need Codec2 (LBW = 3200bps)
|
||||
call_send_signal(LXST_PREFERRED_PROFILE + LXST_PROFILE_LBW);
|
||||
_call_state = CallState::RINGING;
|
||||
_call_timeout_ms = millis() + 60000;
|
||||
_call_screen->set_state(CallScreen::CallState::RINGING);
|
||||
// Send profile preference: LBW (Codec2 3200bps)
|
||||
call_send_signal(LXST_PREFERRED_PROFILE + LXST_PROFILE_LBW);
|
||||
} else if (signal == LXST_STATUS_BUSY || signal == LXST_STATUS_REJECTED) {
|
||||
INFO("LXST: Call rejected or busy");
|
||||
call_ended();
|
||||
@@ -1353,57 +1381,79 @@ void UIManager::call_update() {
|
||||
}
|
||||
}
|
||||
|
||||
// Pump TX: batch exactly 8 codec frames into one packet.
|
||||
// Pump TX: batch codec frames to match Columba's expected ring buffer slot size.
|
||||
// Columba's native OboePlaybackEngine expects exactly frameSamples (1600 for 3200
|
||||
// mode) decoded samples per writeEncodedPacket call = 10 sub-frames.
|
||||
// Each encoded frame from ring buffer = [mode_header(1)] + [raw_codec2(8)] = 9 bytes.
|
||||
// We send exactly 8 frames per packet: [mode_header] + [8 * raw_codec2] = 65 bytes.
|
||||
// MUST be exactly 8: Columba's native ring buffer (frameSamples=2560) rejects
|
||||
// partial writes (count != frameSamples), so 8*320=2560 samples is required.
|
||||
static constexpr int TX_BATCH_SIZE = 10; // frames per packet — MUST match Columba's frameSamples/160 (LBW 3200)
|
||||
// We pack 10 frames per batch: [codec_type(1)] + [mode(1)] + [10*raw(80)] = 82 bytes.
|
||||
// Multiple batches go into a fixarray, matching Columba's wire format exactly.
|
||||
static constexpr int FRAMES_PER_BATCH = 10; // 10 * 160 = 1600 = Columba's frameSamples
|
||||
static constexpr int MAX_BATCHES = 2; // Up to 2 batches per packet (like Columba)
|
||||
static constexpr int TX_MAX_FRAMES = FRAMES_PER_BATCH * MAX_BATCHES;
|
||||
static constexpr uint32_t TX_INTERVAL_MS = 200; // Match LXST-kt LBW frame time
|
||||
static uint32_t last_tx_ms = 0;
|
||||
if (_lxst_audio && _lxst_audio->isCapturing()) {
|
||||
// Send up to 3 batched packets per call_update() cycle
|
||||
for (int batch = 0; batch < 3; batch++) {
|
||||
// Check if we have enough frames before starting to read
|
||||
if (_lxst_audio->capturePacketsAvailable() < TX_BATCH_SIZE) {
|
||||
break; // Wait until we have a full batch
|
||||
}
|
||||
int available = _lxst_audio->capturePacketsAvailable();
|
||||
bool time_to_send = (now - last_tx_ms) >= TX_INTERVAL_MS;
|
||||
// Send if: enough time has passed AND we have frames,
|
||||
// OR ring is getting full (>60 frames = 1.2s buffered)
|
||||
if ((time_to_send && available >= FRAMES_PER_BATCH) || available > 60) {
|
||||
int to_send = available < TX_MAX_FRAMES ? available : TX_MAX_FRAMES;
|
||||
|
||||
uint8_t batch_buf[128]; // [mode_header] + [N * raw_codec2_data]
|
||||
int batch_len = 0;
|
||||
int frame_count = 0;
|
||||
uint8_t encoded_buf[64];
|
||||
int encoded_len = 0;
|
||||
// Read frames and pack into batches of 10.
|
||||
// batch_data: concatenated batches, each 82 bytes:
|
||||
// [codec_type(0x02)] + [mode_header] + [10 * 8 raw bytes]
|
||||
uint8_t batch_data[MAX_BATCHES * 82];
|
||||
int batch_count = 0;
|
||||
int total_frames = 0;
|
||||
uint8_t encoded_buf[16];
|
||||
|
||||
for (int i = 0; i < TX_BATCH_SIZE; i++) {
|
||||
if (!_lxst_audio->readEncodedPacket(encoded_buf, sizeof(encoded_buf), &encoded_len)) {
|
||||
while (batch_count < MAX_BATCHES && to_send >= FRAMES_PER_BATCH) {
|
||||
uint8_t* bp = batch_data + batch_count * 82;
|
||||
bp[0] = LXST_CODEC_CODEC2; // codec_type = 0x02
|
||||
int frames_in_batch = 0;
|
||||
|
||||
for (int i = 0; i < FRAMES_PER_BATCH; i++) {
|
||||
int encoded_len = 0;
|
||||
if (!_lxst_audio->readEncodedPacket(encoded_buf, sizeof(encoded_buf), &encoded_len)) {
|
||||
break;
|
||||
}
|
||||
if (encoded_len < 2) continue;
|
||||
|
||||
if (frames_in_batch == 0) {
|
||||
// First frame: keep mode_header + raw
|
||||
memcpy(bp + 1, encoded_buf, encoded_len);
|
||||
} else {
|
||||
// Subsequent: append raw only (strip mode_header)
|
||||
memcpy(bp + 1 + 1 + frames_in_batch * 8, encoded_buf + 1, encoded_len - 1);
|
||||
}
|
||||
frames_in_batch++;
|
||||
_call_audio_tx_count++;
|
||||
to_send--;
|
||||
}
|
||||
|
||||
if (frames_in_batch == FRAMES_PER_BATCH) {
|
||||
batch_count++;
|
||||
total_frames += frames_in_batch;
|
||||
} else {
|
||||
// Incomplete batch — put back? Can't, so just count what we got
|
||||
total_frames += frames_in_batch;
|
||||
if (frames_in_batch > 0) batch_count++;
|
||||
break;
|
||||
}
|
||||
if (encoded_len < 2) continue; // need at least mode_header + 1 byte
|
||||
|
||||
if (frame_count == 0) {
|
||||
// First frame: keep mode header + data
|
||||
memcpy(batch_buf, encoded_buf, encoded_len);
|
||||
batch_len = encoded_len;
|
||||
} else {
|
||||
// Subsequent frames: strip mode header, append raw data only
|
||||
int raw_len = encoded_len - 1;
|
||||
if (batch_len + raw_len > (int)sizeof(batch_buf)) break;
|
||||
memcpy(batch_buf + batch_len, encoded_buf + 1, raw_len);
|
||||
batch_len += raw_len;
|
||||
}
|
||||
frame_count++;
|
||||
_call_audio_tx_count++;
|
||||
}
|
||||
|
||||
if (frame_count == TX_BATCH_SIZE) {
|
||||
call_send_audio_batch(batch_buf, batch_len, frame_count);
|
||||
if (_call_audio_tx_count <= 16) {
|
||||
char dbg[80];
|
||||
snprintf(dbg, sizeof(dbg), "LXST: TX batch %d frames, %d bytes, total=%lu",
|
||||
frame_count, batch_len, (unsigned long)_call_audio_tx_count);
|
||||
if (batch_count > 0) {
|
||||
call_send_audio_batch(batch_data, 82 * batch_count,
|
||||
batch_count, total_frames);
|
||||
last_tx_ms = now;
|
||||
if (_call_audio_tx_count <= 50) {
|
||||
char dbg[96];
|
||||
snprintf(dbg, sizeof(dbg), "LXST: TX %d batches %d frames (%d avail), total=%lu",
|
||||
batch_count, total_frames, available,
|
||||
(unsigned long)_call_audio_tx_count);
|
||||
INFO(dbg);
|
||||
}
|
||||
} else {
|
||||
break; // couldn't fill batch (shouldn't happen after availablePackets check)
|
||||
}
|
||||
}
|
||||
} else if (_call_audio_tx_count == 0) {
|
||||
|
||||
@@ -246,7 +246,7 @@ private:
|
||||
static constexpr uint8_t LXST_STATUS_CONNECTING = 0x05;
|
||||
static constexpr uint8_t LXST_STATUS_ESTABLISHED = 0x06;
|
||||
|
||||
// LXST codec header byte
|
||||
// LXST codec type bytes (match LXST Codecs/__init__.py)
|
||||
static constexpr uint8_t LXST_CODEC_CODEC2 = 0x02;
|
||||
|
||||
// LXST profile negotiation
|
||||
@@ -295,8 +295,8 @@ private:
|
||||
// Send a signalling byte over the call link
|
||||
void call_send_signal(int signal);
|
||||
|
||||
// Send batched encoded audio frames over the call link
|
||||
void call_send_audio_batch(const uint8_t* batch_data, int batch_len, int frame_count);
|
||||
// Send batched audio frames over the call link (10 sub-frames per batch)
|
||||
void call_send_audio_batch(const uint8_t* batch_data, int batch_len, int batch_count, int total_frames);
|
||||
|
||||
// Process a single received audio frame (codec_header + data)
|
||||
void call_rx_audio_frame(const uint8_t* frame, size_t frame_len);
|
||||
|
||||
Reference in New Issue
Block a user