From 44b5042c3503068e078740a88e327425ed859bc5 Mon Sep 17 00:00:00 2001 From: Bryan Biedenkapp Date: Fri, 25 Sep 2026 17:40:03 -0400 Subject: [PATCH] begin some corrective work for bridge NXDN support; --- src/bridge/HostBridge.NXDN.cpp | 89 +++++++++++++++++++++++++--------- src/bridge/HostBridge.cpp | 41 +++++++++++++--- src/vocoder/MBEDecoder.cpp | 27 +++++++++++ src/vocoder/MBEDecoder.h | 8 +++ src/vocoder/MBEEncoder.cpp | 20 ++++++++ src/vocoder/MBEEncoder.h | 7 +++ 6 files changed, 161 insertions(+), 31 deletions(-) diff --git a/src/bridge/HostBridge.NXDN.cpp b/src/bridge/HostBridge.NXDN.cpp index 9a2f2b43..62d53b3b 100644 --- a/src/bridge/HostBridge.NXDN.cpp +++ b/src/bridge/HostBridge.NXDN.cpp @@ -142,6 +142,9 @@ void HostBridge::processNXDNNetwork(uint8_t* buffer, uint32_t length) m_rxNXDNLC = lc::RTCH(); m_rxStartTime = 0U; m_rxStreamId = 0U; + m_nxdnSeqNo = 0U; + m_nxdnN = 0U; + ::memset(m_nxdnAMBE, 0x00U, 36U); if (!m_udpRTPContinuousSeq) { m_rtpInitialFrame = false; @@ -157,6 +160,26 @@ void HostBridge::processNXDNNetwork(uint8_t* buffer, uint32_t length) if (m_ignoreCall) return; + // Only superframe SACCH traffic frames contain voice payloads. + if (fct != FuncChannelType::USC_SACCH_SS) + return; + + // A bridge may join an already active stream after its VCALL header. Treat + // the first valid voice frame as the start of the receive call as well. + if (!m_callInProgress) { + m_callInProgress = true; + m_callAlgoId = 0U; + m_networkWatchdog.start(); + m_rxStartTime = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + LogInfoEx(LOG_HOST, "NXDN, late entry call start, srcId = %u, dstId = %u", srcId, dstId); + if (m_preambleLeaderTone) + generatePreambleTone(); + } + else { + m_networkWatchdog.start(); + } + LogInfoEx(LOG_NET, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL ", audio, srcId = %u, dstId = %u", srcId, dstId); decodeNXDNAudioFrame(frame, srcId, dstId, m_nxdnSeqNo); @@ -187,39 +210,52 @@ void HostBridge::decodeNXDNAudioFrame(uint8_t* frame, uint32_t srcId, uint32_t d ::memset(nxdnAMBE, 0x00U, 18U); ::memcpy(nxdnAMBE, frame + 2U + NXDN_FSW_LICH_SACCH_LENGTH_BYTES + pairOffset, 18U); - ambeFec.regenerateNXDN(nxdnAMBE + 0U); - ambeFec.regenerateNXDN(nxdnAMBE + 9U); + const uint32_t fecErrors[] = { + ambeFec.regenerateNXDN(nxdnAMBE + 0U), + ambeFec.regenerateNXDN(nxdnAMBE + 9U) + }; + + // match the normal NXDN voice path's lost-audio policy -- a pair is + // half of a full four-codeword frame, so use half the frame threshold + if ((fecErrors[0U] + fecErrors[1U]) > (DEFAULT_SILENCE_THRESHOLD / 2U)) { + ::memcpy(nxdnAMBE + 0U, NULL_AMBE, 9U); + ::memcpy(nxdnAMBE + 9U, NULL_AMBE, 9U); + LogWarning(LOG_HOST, "NXDN, AMBE errors exceeded threshold, substituting silence, errors = %u", + fecErrors[0U] + fecErrors[1U]); + } uint8_t packedBits[13U]; ::memset(packedBits, 0x00U, 13U); nxdnAudio.decode(nxdnAMBE, packedBits); for (uint8_t half = 0U; half < 2U; half++) { - uint8_t rawBits[72U]; - ::memset(rawBits, 0x00U, 72U); + uint8_t rawBits[49U]; + ::memset(rawBits, 0x00U, sizeof(rawBits)); for (uint32_t b = 0U; b < 49U; b++) { rawBits[b] = READ_BIT(packedBits, (half * 49U) + b) ? 1U : 0U; } - // HACK: use the DMR AMBE handling to decode NXDN audio - uint8_t dmrAMBE[dmr::defines::DMR_AMBE_LENGTH_BYTES]; - ::memset(dmrAMBE, 0x00U, dmr::defines::DMR_AMBE_LENGTH_BYTES); - m_encoder->encodeBits(rawBits, dmrAMBE); - short samples[AUDIO_SAMPLES_LENGTH]; int errs = 0; #if defined(_WIN32) if (m_useExternalVocoder) { - ambeDecode(dmrAMBE, dmr::defines::DMR_AMBE_LENGTH_BYTES, samples); + // reframe the NXDN 49-bit payload before handing it to the external vocoder + uint8_t ambePartial[dmr::defines::RAW_AMBE_LENGTH_BYTES]; + ::memset(ambePartial, 0x00U, sizeof(ambePartial)); + m_encoder->encodeBits(rawBits, ambePartial); + + errs = ambeDecode(ambePartial, dmr::defines::RAW_AMBE_LENGTH_BYTES, samples); } else { #endif // defined(_WIN32) - errs = m_decoder->decode(dmrAMBE, samples); + errs = m_decoder->decodeBits(rawBits, samples); #if defined(_WIN32) } #endif // defined(_WIN32) + errs += (int)fecErrors[half]; + if (m_debug) { LogDebug(LOG_HOST, "NXDN, Frame, VC%u.%u, srcId = %u, dstId = %u, errs = %u", nxdnN, vcBase + half, srcId, dstId, errs); } @@ -306,16 +342,25 @@ void HostBridge::encodeNXDNAudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32 AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_txAudioGain); - // HACK: use the DMR AMBE handling to encode NXDN audio - uint8_t dmrAMBE[dmr::defines::DMR_AMBE_LENGTH_BYTES]; - ::memset(dmrAMBE, 0x00U, dmr::defines::DMR_AMBE_LENGTH_BYTES); + uint8_t rawBits[49U]; + ::memset(rawBits, 0x00U, sizeof(rawBits)); #if defined(_WIN32) if (m_useExternalVocoder) { - ambeEncode(samples, AUDIO_SAMPLES_LENGTH, dmrAMBE); + // reframe the NXDN 49-bit payload before handing it to the external vocoder + uint8_t ambePartial[dmr::defines::RAW_AMBE_LENGTH_BYTES]; + ::memset(ambePartial, 0x00U, sizeof(ambePartial)); + ambeEncode(samples, AUDIO_SAMPLES_LENGTH, ambePartial); + + char mbeBits[49U]; + ::memset(mbeBits, 0x00U, sizeof(mbeBits)); + m_decoder->decodeBits(ambePartial, mbeBits); + + for (uint32_t i = 0U; i < 49U; i++) + rawBits[i] = mbeBits[i] != 0; } else { #endif // defined(_WIN32) - m_encoder->encode(samples, dmrAMBE); + m_encoder->encodeBits(samples, rawBits); #if defined(_WIN32) } #endif // defined(_WIN32) @@ -324,7 +369,10 @@ void HostBridge::encodeNXDNAudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32 m_nxdnN = 0U; } - ::memcpy(m_nxdnAMBE + (m_nxdnN * dmr::defines::DMR_AMBE_LENGTH_BYTES), dmrAMBE, dmr::defines::DMR_AMBE_LENGTH_BYTES); + uint8_t* packedAMBE = m_nxdnAMBE + (m_nxdnN * dmr::defines::DMR_AMBE_LENGTH_BYTES); + ::memset(packedAMBE, 0x00U, dmr::defines::DMR_AMBE_LENGTH_BYTES); + for (uint32_t b = 0U; b < 49U; b++) + WRITE_BIT(packedAMBE, b, rawBits[b] != 0U); m_nxdnN++; if (m_nxdnN < 4U) { @@ -341,14 +389,9 @@ void HostBridge::encodeNXDNAudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32 ::memset(packedBits, 0x00U, 13U); for (uint8_t half = 0U; half < 2U; half++) { - char mbeBits[49U]; - ::memset(mbeBits, 0x00U, 49U); - uint8_t* ambe = m_nxdnAMBE + ((pair * 2U + half) * dmr::defines::DMR_AMBE_LENGTH_BYTES); - m_decoder->decodeBits(ambe, mbeBits); - for (uint32_t b = 0U; b < 49U; b++) { - WRITE_BIT(packedBits, (half * 49U) + b, mbeBits[b] != 0); + WRITE_BIT(packedBits, (half * 49U) + b, READ_BIT(ambe, b)); } } diff --git a/src/bridge/HostBridge.cpp b/src/bridge/HostBridge.cpp index 72f5eaf2..de874a97 100644 --- a/src/bridge/HostBridge.cpp +++ b/src/bridge/HostBridge.cpp @@ -18,6 +18,7 @@ #include "common/dmr/data/EMB.h" #include "common/nxdn/NXDNDefines.h" #include "common/nxdn/NXDNUtils.h" +#include "common/nxdn/Audio.h" #include "common/nxdn/Sync.h" #include "common/nxdn/channel/FACCH1.h" #include "common/nxdn/channel/LICH.h" @@ -2993,13 +2994,24 @@ void HostBridge::padSilenceAudio(uint32_t srcId, uint32_t dstId) using namespace nxdn; using namespace nxdn::defines; - if (m_nxdnN == 0U || m_nxdnN > 3U) { - ::memset(m_nxdnAMBE, 0x00U, 36U); - - ::memcpy(m_nxdnAMBE + 0U, NULL_AMBE, RAW_AMBE_LENGTH_BYTES); - ::memcpy(m_nxdnAMBE + 9U, NULL_AMBE, RAW_AMBE_LENGTH_BYTES); - ::memcpy(m_nxdnAMBE + 18U, NULL_AMBE, RAW_AMBE_LENGTH_BYTES); - ::memcpy(m_nxdnAMBE + 27U, NULL_AMBE, RAW_AMBE_LENGTH_BYTES); + if (m_nxdnN > 0U && m_nxdnN < 4U) { + // m_nxdnAMBE stores packed raw 49-bit payloads. Pad the + // incomplete frame with the raw payload represented by the + // standard NXDN null codeword. + nxdn::Audio nxdnAudio; + uint8_t nullPair[18U]; + ::memcpy(nullPair + 0U, NULL_AMBE, RAW_AMBE_LENGTH_BYTES); + ::memcpy(nullPair + 9U, NULL_AMBE, RAW_AMBE_LENGTH_BYTES); + uint8_t nullBits[13U]; + ::memset(nullBits, 0x00U, sizeof(nullBits)); + nxdnAudio.decode(nullPair, nullBits); + + for (uint8_t n = m_nxdnN; n < 4U; n++) { + uint8_t* packedAMBE = m_nxdnAMBE + (n * RAW_AMBE_LENGTH_BYTES); + ::memset(packedAMBE, 0x00U, RAW_AMBE_LENGTH_BYTES); + for (uint32_t bit = 0U; bit < 49U; bit++) + WRITE_BIT(packedAMBE, bit, READ_BIT(nullBits, bit)); + } ::nxdn::lc::RTCH lc = ::nxdn::lc::RTCH(); lc.setMessageType(NXDDEF::MessageType::RTCH_VCALL); @@ -3043,7 +3055,20 @@ void HostBridge::padSilenceAudio(uint32_t srcId, uint32_t dstId) sacch.setStructure(structures[superframeIndex]); sacch.encode(frame); - ::memcpy(frame + NXDN_FSW_LICH_SACCH_LENGTH_BYTES, m_nxdnAMBE, 36U); + uint8_t nxdnAudioPayload[36U]; + ::memset(nxdnAudioPayload, 0x00U, sizeof(nxdnAudioPayload)); + for (uint8_t pair = 0U; pair < 2U; pair++) { + uint8_t packedBits[13U]; + ::memset(packedBits, 0x00U, sizeof(packedBits)); + for (uint8_t half = 0U; half < 2U; half++) { + const uint8_t* ambe = m_nxdnAMBE + ((pair * 2U + half) * RAW_AMBE_LENGTH_BYTES); + for (uint32_t bit = 0U; bit < 49U; bit++) + WRITE_BIT(packedBits, (half * 49U) + bit, READ_BIT(ambe, bit)); + } + nxdnAudio.encode(packedBits, nxdnAudioPayload + (pair * 18U)); + } + + ::memcpy(frame + NXDN_FSW_LICH_SACCH_LENGTH_BYTES, nxdnAudioPayload, sizeof(nxdnAudioPayload)); NXDNUtils::scrambler(frame); m_network->writeNXDN(lc, frame, NXDN_FRAME_LENGTH_BYTES); diff --git a/src/vocoder/MBEDecoder.cpp b/src/vocoder/MBEDecoder.cpp index 32f5084a..94cb6423 100644 --- a/src/vocoder/MBEDecoder.cpp +++ b/src/vocoder/MBEDecoder.cpp @@ -15,6 +15,8 @@ #include "common/edac/Golay24128.h" #include "vocoder/MBEDecoder.h" +#include + using namespace edac; using namespace vocoder; @@ -152,6 +154,31 @@ int32_t MBEDecoder::decodeBits(uint8_t* codeword, char* mbeBits) return errs; } +/* Decodes a deinterleaved 49-bit AMBE payload to PCM samples. */ + +int32_t MBEDecoder::decodeBits(const uint8_t* mbeBits, int16_t samples[]) +{ + assert(mbeBits != nullptr); + assert(samples != nullptr); + + if (m_mbeMode != DECODE_DMR_AMBE) + return -1; + + char ambe[49U]; + for (uint32_t i = 0U; i < 49U; i++) + ambe[i] = mbeBits[i] != 0U ? 1 : 0; + + int errs = 0; + int errs2 = 0; + char errStr[64U]; + ::memset(errStr, 0x20U, sizeof(errStr)); + mbe_processAmbe2450Data(samples, &errs, &errs2, errStr, ambe, + m_mbelibParms->m_cur_mp, m_mbelibParms->m_prev_mp, + m_mbelibParms->m_prev_mp_enhanced, 3); + + return errs2; +} + /* Decodes the given MBE codewords to PCM samples using the decoder mode. */ int32_t MBEDecoder::decodeF(uint8_t* codeword, float samples[]) diff --git a/src/vocoder/MBEDecoder.h b/src/vocoder/MBEDecoder.h index 88c0d88a..1db19189 100644 --- a/src/vocoder/MBEDecoder.h +++ b/src/vocoder/MBEDecoder.h @@ -95,6 +95,14 @@ namespace vocoder */ int32_t decodeBits(uint8_t* codeword, char* mbeBits); + /** + * @brief Decodes a deinterleaved 49-bit AMBE payload to PCM samples. + * @param[in] mbeBits Deinterleaved AMBE payload, one bit per byte. + * @param[out] samples PCM samples. + * @returns int32_t Decoder error count. + */ + int32_t decodeBits(const uint8_t* mbeBits, int16_t samples[]); + /** * @brief Decodes the given MBE codewords to PCM samples using the decoder mode. * @param[in] codeword MBE codeword. diff --git a/src/vocoder/MBEEncoder.cpp b/src/vocoder/MBEEncoder.cpp index de5152ff..5537db81 100644 --- a/src/vocoder/MBEEncoder.cpp +++ b/src/vocoder/MBEEncoder.cpp @@ -715,3 +715,23 @@ void MBEEncoder::encode(int16_t* samples, uint8_t* codeword) ::memcpy(codeword, dmrAMBE, 9U); } } + +/* Encodes PCM samples to a deinterleaved 49-bit AMBE payload. */ + +void MBEEncoder::encodeBits(int16_t* samples, uint8_t bits[]) +{ + assert(samples != nullptr); + assert(bits != nullptr); + + if (m_mbeMode != ENCODE_DMR_AMBE) { + ::memset(bits, 0x00U, 49U); + return; + } + + int16_t frameVector[8]; + m_vocoder.imbe_encode(frameVector, samples); + + int b[9]; + encodeAMBE(m_vocoder.param(), b, &m_curMBEParms, &m_prevMBEParms, m_gainAdjust); + encode49bit(bits, b); +} diff --git a/src/vocoder/MBEEncoder.h b/src/vocoder/MBEEncoder.h index 55fa02b5..f216ac02 100644 --- a/src/vocoder/MBEEncoder.h +++ b/src/vocoder/MBEEncoder.h @@ -66,6 +66,13 @@ namespace vocoder */ void encode(int16_t* samples, uint8_t* codeword); + /** + * @brief Encodes PCM samples to a deinterleaved 49-bit AMBE payload. + * @param[in] samples PCM samples. + * @param[out] bits Deinterleaved AMBE payload, one bit per byte. + */ + void encodeBits(int16_t* samples, uint8_t bits[]); + private: imbe_vocoder m_vocoder; mbe_parms m_curMBEParms;