// SPDX-License-Identifier: GPL-2.0-only /* * Digital Voice Modem - Bridge * GPLv2 Open Source. Use is subject to license terms. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * Copyright (C) 2026 Bryan Biedenkapp, N2PLL * */ #include "Defines.h" #include "common/analog/AnalogDefines.h" #include "common/analog/AnalogAudio.h" #include "common/dmr/DMRDefines.h" #include "common/edac/AMBEFEC.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" #include "common/nxdn/channel/SACCH.h" #include "common/nxdn/lc/RTCH.h" #include "common/Log.h" #include "common/Utils.h" #include "bridge/ActivityLog.h" #include "HostBridge.h" #include "BridgeMain.h" using namespace analog; using namespace analog::defines; using namespace network; using namespace network::frame; using namespace network::udp; #include #include #include #include // --------------------------------------------------------------------------- // Public Class Members // --------------------------------------------------------------------------- /* ** NXDN */ /* Helper to process NXDN network traffic. */ void HostBridge::processNXDNNetwork(uint8_t* buffer, uint32_t length) { assert(buffer != nullptr); using namespace nxdn; using namespace nxdn::defines; if (m_txMode != TX_MODE_NXDN) { m_network->resetNXDN(); return; } uint8_t messageType = buffer[4U]; uint32_t srcId = GET_UINT24(buffer, 5U); uint32_t dstId = GET_UINT24(buffer, 8U); if (m_debug) { LogDebug(LOG_NET, "NXDN, messageType = $%02X, srcId = %u, dstId = %u, len = %u", messageType, srcId, dstId, length); } if (m_audioDetect || m_trafficFromUDP) return; if (srcId == 0U) { m_network->resetNXDN(); return; } if (dstId != m_dstId) { m_network->resetNXDN(); return; } uint8_t frameLength = buffer[23U]; if (frameLength == 0U) { m_network->resetNXDN(); return; } uint32_t payloadLength = frameLength; if (payloadLength > NXDN_FRAME_LENGTH_BYTES && payloadLength >= 24U) payloadLength -= 24U; if (payloadLength < NXDN_FRAME_LENGTH_BYTES) { m_network->resetNXDN(); return; } uint8_t frame[NXDN_FRAME_LENGTH_BYTES + 2U]; ::memset(frame, 0x00U, NXDN_FRAME_LENGTH_BYTES + 2U); ::memcpy(frame + 2U, buffer + 24U, NXDN_FRAME_LENGTH_BYTES); NXDNUtils::scrambler(frame + 2U); channel::LICH lich; if (!lich.decode(frame + 2U)) { m_network->resetNXDN(); return; } FuncChannelType::E fct = lich.getFCT(); // process non-superframe control signalling (VCALL/TX_REL) from FACCH if (fct == FuncChannelType::USC_SACCH_NS) { channel::FACCH1 facch; bool valid = facch.decode(frame + 2U, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS); if (!valid) { valid = facch.decode(frame + 2U, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS + NXDN_FACCH1_FEC_LENGTH_BITS); } if (valid) { uint8_t lcBuffer[10U]; ::memset(lcBuffer, 0x00U, 10U); facch.getData(lcBuffer); lc::RTCH lc; lc.decode(lcBuffer, NXDN_FACCH1_FEC_LENGTH_BITS); if (lc.getMessageType() == MessageType::RTCH_VCALL) { m_rxNXDNLC = lc; m_networkWatchdog.start(); if (m_network->getNXDNStreamId() != m_rxStreamId && !m_callInProgress) { m_callInProgress = true; m_callAlgoId = 0U; uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); m_rxStartTime = now; LogInfoEx(LOG_HOST, "NXDN, call start, srcId = %u, dstId = %u", srcId, dstId); if (m_preambleLeaderTone) generatePreambleTone(); } } else if (lc.getMessageType() == MessageType::RTCH_TX_REL || lc.getMessageType() == MessageType::RTCH_TX_REL_EX) { m_callInProgress = false; m_networkWatchdog.stop(); m_ignoreCall = false; m_callAlgoId = 0U; if (m_rxStartTime > 0U) { uint64_t now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); uint64_t diff = now - m_rxStartTime; LogInfoEx(LOG_HOST, "NXDN, call end, srcId = %u, dstId = %u, dur = %us", srcId, dstId, diff / 1000U); } m_rxNXDNLC = lc::RTCH(); m_rxStartTime = 0U; m_rxStreamId = 0U; if (!m_udpRTPContinuousSeq) { m_rtpInitialFrame = false; m_rtpSeqNo = 0U; } m_rtpTimestamp = INVALID_TS; m_network->resetNXDN(); return; } } } if (m_ignoreCall) return; LogInfoEx(LOG_NET, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL ", audio, srcId = %u, dstId = %u", srcId, dstId); decodeNXDNAudioFrame(frame, srcId, dstId, m_nxdnSeqNo); m_nxdnSeqNo++; m_rxStreamId = m_network->getNXDNStreamId(); } /* Helper to decode NXDN network traffic audio frames. */ void HostBridge::decodeNXDNAudioFrame(uint8_t* frame, uint32_t srcId, uint32_t dstId, uint8_t nxdnN) { assert(frame != nullptr); using namespace nxdn; using namespace nxdn::defines; channel::LICH lich; if (!lich.decode(frame + 2U)) return; ChOption::E option = lich.getOption(); nxdn::Audio nxdnAudio; ::edac::AMBEFEC ambeFec; // ahhh yes fantastical C++ lambda functions... auto decodePair = [&](uint32_t pairOffset, uint8_t vcBase) { uint8_t nxdnAMBE[18U]; ::memset(nxdnAMBE, 0x00U, 18U); ::memcpy(nxdnAMBE, frame + 2U + NXDN_FSW_LICH_SACCH_LENGTH_BYTES + pairOffset, 18U); ambeFec.regenerateNXDN(nxdnAMBE + 0U); ambeFec.regenerateNXDN(nxdnAMBE + 9U); 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); 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); } else { #endif // defined(_WIN32) errs = m_decoder->decode(dmrAMBE, samples); #if defined(_WIN32) } #endif // defined(_WIN32) if (m_debug) { LogDebug(LOG_HOST, "NXDN, Frame, VC%u.%u, srcId = %u, dstId = %u, errs = %u", nxdnN, vcBase + half, srcId, dstId, errs); } AnalogAudio::gain(samples, AUDIO_SAMPLES_LENGTH, m_rxAudioGain); if (m_localAudio) { m_outputAudio.addData(samples, AUDIO_SAMPLES_LENGTH); assertRtsPtt(); } if (m_udpAudio) { int pcmIdx = 0; uint8_t pcm[AUDIO_SAMPLES_LENGTH * 2U]; if (m_udpUseULaw) { for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) { pcm[smpIdx] = AnalogAudio::encodeMuLaw(samples[smpIdx]); } if (m_trace) Utils::dump(1U, "HostBridge()::decodeNXDNAudioFrame(), Encoded uLaw Audio", pcm, AUDIO_SAMPLES_LENGTH); writeUDPAudio(srcId, dstId, pcm, AUDIO_SAMPLES_LENGTH_BYTES / 2U); } else { for (uint32_t smpIdx = 0; smpIdx < AUDIO_SAMPLES_LENGTH; smpIdx++) { pcm[pcmIdx + 0] = (uint8_t)(samples[smpIdx] & 0xFF); pcm[pcmIdx + 1] = (uint8_t)((samples[smpIdx] >> 8) & 0xFF); pcmIdx += 2; } writeUDPAudio(srcId, dstId, pcm, AUDIO_SAMPLES_LENGTH_BYTES); } } } }; switch (option) { case ChOption::STEAL_NONE: decodePair(0U, 0U); decodePair(18U, 2U); break; case ChOption::STEAL_FACCH1_1: decodePair(18U, 2U); break; case ChOption::STEAL_FACCH1_2: decodePair(0U, 0U); break; case ChOption::STEAL_FACCH: default: break; } } /* Helper to encode NXDN network traffic audio frames. */ void HostBridge::encodeNXDNAudioFrame(uint8_t* pcm, uint32_t forcedSrcId, uint32_t forcedDstId) { assert(pcm != nullptr); using namespace nxdn; using namespace nxdn::defines; uint32_t srcId = m_srcId; if (m_srcIdOverride != 0 && (m_overrideSrcIdFromMDC)) srcId = m_srcIdOverride; if (m_overrideSrcIdFromUDP) srcId = m_udpSrcId; if (forcedSrcId > 0 && forcedSrcId != m_srcId) srcId = forcedSrcId; uint32_t dstId = m_dstId; if (forcedDstId > 0 && forcedDstId != m_dstId) dstId = forcedDstId; if (srcId == 0U) srcId = m_srcId; int smpIdx = 0; short samples[AUDIO_SAMPLES_LENGTH]; for (uint32_t pcmIdx = 0; pcmIdx < (AUDIO_SAMPLES_LENGTH * 2U); pcmIdx += 2) { samples[smpIdx] = (short)((pcm[pcmIdx + 1] << 8) + pcm[pcmIdx + 0]); smpIdx++; } 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); #if defined(_WIN32) if (m_useExternalVocoder) { ambeEncode(samples, AUDIO_SAMPLES_LENGTH, dmrAMBE); } else { #endif // defined(_WIN32) m_encoder->encode(samples, dmrAMBE); #if defined(_WIN32) } #endif // defined(_WIN32) if (m_nxdnN >= 4U) { m_nxdnN = 0U; } ::memcpy(m_nxdnAMBE + (m_nxdnN * dmr::defines::DMR_AMBE_LENGTH_BYTES), dmrAMBE, dmr::defines::DMR_AMBE_LENGTH_BYTES); m_nxdnN++; if (m_nxdnN < 4U) { return; } nxdn::Audio nxdnAudio; uint8_t nxdnAudioPayload[36U]; ::memset(nxdnAudioPayload, 0x00U, 36U); for (uint8_t pair = 0U; pair < 2U; pair++) { uint8_t packedBits[13U]; ::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); } } uint8_t encodedPair[18U]; ::memset(encodedPair, 0x00U, 18U); nxdnAudio.encode(packedBits, encodedPair); ::memcpy(nxdnAudioPayload + (pair * 18U), encodedPair, 18U); } lc::RTCH lc = lc::RTCH(); lc.setMessageType(MessageType::RTCH_VCALL); lc.setCallType(CallType::UNSPECIFIED); lc.setGroup(true); lc.setSrcId((uint16_t)srcId); lc.setDstId((uint16_t)dstId); lc.setTransmissionMode(TransmissionMode::MODE_4800); if (m_nxdnSeqNo == 0U) { uint8_t controlFrame[NXDN_FRAME_LENGTH_BYTES + 2U]; ::memset(controlFrame, 0x00U, NXDN_FRAME_LENGTH_BYTES + 2U); Sync::addNXDNSync(controlFrame + 2U); channel::LICH lich; lich.setRFCT(RFChannelType::RDCH); lich.setFCT(FuncChannelType::USC_SACCH_NS); lich.setOption(ChOption::STEAL_FACCH); lich.setOutbound(true); lich.encode(controlFrame + 2U); channel::SACCH sacch; sacch.setData(SACCH_IDLE); sacch.setRAN(0U); sacch.setStructure(ChStructure::SR_SINGLE); sacch.encode(controlFrame + 2U); channel::FACCH1 facch; uint8_t lcData[NXDN_RTCH_LC_LENGTH_BYTES]; ::memset(lcData, 0x00U, NXDN_RTCH_LC_LENGTH_BYTES); lc.encode(lcData, NXDN_RTCH_LC_LENGTH_BITS); facch.setData(lcData); facch.encode(controlFrame + 2U, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS); facch.encode(controlFrame + 2U, NXDN_FSW_LENGTH_BITS + NXDN_LICH_LENGTH_BITS + NXDN_SACCH_FEC_LENGTH_BITS + NXDN_FACCH1_FEC_LENGTH_BITS); NXDNUtils::scrambler(controlFrame + 2U); LogInfoEx(LOG_HOST, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL ", srcId = %u, dstId = %u", srcId, dstId); m_network->writeNXDN(lc, controlFrame + 2U, NXDN_FRAME_LENGTH_BYTES); m_txStreamId = m_network->getNXDNStreamId(); } uint8_t voiceFrame[NXDN_FRAME_LENGTH_BYTES + 2U]; ::memset(voiceFrame, 0x00U, NXDN_FRAME_LENGTH_BYTES + 2U); Sync::addNXDNSync(voiceFrame + 2U); channel::LICH lich; lich.setRFCT(RFChannelType::RDCH); lich.setFCT(FuncChannelType::USC_SACCH_SS); lich.setOption(ChOption::STEAL_NONE); lich.setOutbound(true); lich.encode(voiceFrame + 2U); channel::SACCH sacch; sacch.setData(SACCH_IDLE); sacch.setRAN(0U); sacch.setStructure(ChStructure::SR_SINGLE); sacch.encode(voiceFrame + 2U); ::memcpy(voiceFrame + 2U + NXDN_FSW_LICH_SACCH_LENGTH_BYTES, nxdnAudioPayload, 36U); NXDNUtils::scrambler(voiceFrame + 2U); LogInfoEx(LOG_HOST, "NXDN, " NXDN_RTCH_MSG_TYPE_VCALL ", audio, srcId = %u, dstId = %u", srcId, dstId); m_network->writeNXDN(lc, voiceFrame + 2U, NXDN_FRAME_LENGTH_BYTES); m_txStreamId = m_network->getNXDNStreamId(); m_nxdnSeqNo++; m_nxdnN = 0U; ::memset(m_nxdnAMBE, 0x00U, 36U); }