From ddd19a04dbb46bd0b0dc8debb629ffe1035238aa Mon Sep 17 00:00:00 2001 From: torlando-tech Date: Tue, 24 Feb 2026 16:15:17 -0500 Subject: [PATCH] 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 --- lib/lxst_audio/i2s_capture.cpp | 60 +++++---- lib/lxst_audio/i2s_capture.h | 4 +- lib/lxst_audio/lxst_audio.cpp | 32 +++++ lib/tdeck_ui/UI/LXMF/UIManager.cpp | 204 ++++++++++++++++++----------- lib/tdeck_ui/UI/LXMF/UIManager.h | 6 +- 5 files changed, 201 insertions(+), 105 deletions(-) diff --git a/lib/lxst_audio/i2s_capture.cpp b/lib/lxst_audio/i2s_capture.cpp index e37a5e7d..d474b259 100644 --- a/lib/lxst_audio/i2s_capture.cpp +++ b/lib/lxst_audio/i2s_capture.cpp @@ -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++; } } diff --git a/lib/lxst_audio/i2s_capture.h b/lib/lxst_audio/i2s_capture.h index 2b43c60f..50335bac 100644 --- a/lib/lxst_audio/i2s_capture.h +++ b/lib/lxst_audio/i2s_capture.h @@ -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; }; diff --git a/lib/lxst_audio/lxst_audio.cpp b/lib/lxst_audio/lxst_audio.cpp index 467c1bd7..0848382e 100644 --- a/lib/lxst_audio/lxst_audio.cpp +++ b/lib/lxst_audio/lxst_audio.cpp @@ -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(); diff --git a/lib/tdeck_ui/UI/LXMF/UIManager.cpp b/lib/tdeck_ui/UI/LXMF/UIManager.cpp index 81099900..709fd7dc 100644 --- a/lib/tdeck_ui/UI/LXMF/UIManager.cpp +++ b/lib/tdeck_ui/UI/LXMF/UIManager.cpp @@ -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) { diff --git a/lib/tdeck_ui/UI/LXMF/UIManager.h b/lib/tdeck_ui/UI/LXMF/UIManager.h index 7847946d..6113be9f 100644 --- a/lib/tdeck_ui/UI/LXMF/UIManager.h +++ b/lib/tdeck_ui/UI/LXMF/UIManager.h @@ -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);