diff --git a/configs/bridge-config.example.yml b/configs/bridge-config.example.yml index 5708073a..1a918baa 100644 --- a/configs/bridge-config.example.yml +++ b/configs/bridge-config.example.yml @@ -68,6 +68,7 @@ network: udpReceivePort: 32001 # PCM over UDP receive address. udpReceiveAddress: "127.0.0.1" + # Flag indicating UDP audio should be encoded using G.711 uLaw. udpUseULaw: false # Flag indicating UDP audio should be transmitted without the length leader. @@ -79,6 +80,9 @@ network: # Flag indicating UDP audio should follow the USRP format. udpUsrp: false + # Flag indicating the UDP audio will be padded with silence during hang time before end of call. + udpHangSilence: true + # Traffic Encryption tek: # Flag indicating whether or not traffic encryption is enabled. @@ -135,7 +139,7 @@ system: # Relative sample level for VOX to activate. voxSampleLevel: 80.0 - # Amount of time (ms) from loss of active VOX level to drop audio. + # Amount of time (ms) from loss of active call (loss of local VOX or end of UDP audio stream) before ending call. dropTimeMs: 180 # Enables detection of MDC1200 packets on the PCM side of the bridge. diff --git a/src/bridge/HostBridge.cpp b/src/bridge/HostBridge.cpp index 0b4b0def..ef3cb03a 100644 --- a/src/bridge/HostBridge.cpp +++ b/src/bridge/HostBridge.cpp @@ -298,6 +298,7 @@ HostBridge::HostBridge(const std::string& confFile) : m_udpUseULaw(false), m_udpRTPFrames(false), m_udpUsrp(false), + m_udpSilenceDuringHang(true), m_tekAlgoId(p25::defines::ALGO_UNENCRYPT), m_tekKeyId(0U), m_requestedTek(false), @@ -318,7 +319,10 @@ HostBridge::HostBridge(const std::string& confFile) : m_txMode(1U), m_voxSampleLevel(30.0f), m_dropTimeMS(180U), - m_dropTime(1000U, 0U, 180U), + m_localDropTime(1000U, 0U, 180U), + m_udpCallClock(1000U, 0U, 80U), + m_udpHangTime(1000U, 0U, 180U), + m_udpDropTime(1000U, 0U, 180U), m_detectAnalogMDC1200(false), m_preambleLeaderTone(false), m_preambleTone(2175), @@ -976,8 +980,31 @@ bool HostBridge::readParams() m_txMode = TX_MODE_P25; m_voxSampleLevel = systemConf["voxSampleLevel"].as(30.0f); - m_dropTimeMS = (uint16_t)systemConf["dropTimeMs"].as(180); - m_dropTime = Timer(1000U, 0U, m_dropTimeMS); + m_dropTimeMS = (uint16_t)systemConf["dropTimeMs"].as(180U); + + yaml::Node networkConf = m_conf["network"]; + m_udpAudio = networkConf["udpAudio"].as(false); + bool udpSilenceDuringHang = networkConf["udpHangSilence"].as(true); + + switch (m_txMode) { + case TX_MODE_DMR: + break; + case TX_MODE_P25: + { + if (m_udpAudio && udpSilenceDuringHang && m_dropTimeMS < 360U) { + ::LogWarning(LOG_HOST, "When using UDP silence during hang time, the minimum allowable drop time is 360ms."); + m_dropTimeMS = 360U; // drop time for UDP is minimum 360ms when using silence during hang time + } + } + break; + } + + m_localDropTime = Timer(1000U, 0U, m_dropTimeMS); + m_udpDropTime = Timer(1000U, 0U, m_dropTimeMS); + + // bryanb: UDP drop timer cannot be less then 180ms + if (m_dropTimeMS > 180U) + m_udpDropTime = Timer(1000U, 0U, m_dropTimeMS); m_detectAnalogMDC1200 = systemConf["detectAnalogMDC1200"].as(false); @@ -991,9 +1018,6 @@ bool HostBridge::readParams() m_localAudio = systemConf["localAudio"].as(true); - yaml::Node networkConf = m_conf["network"]; - m_udpAudio = networkConf["udpAudio"].as(false); - m_trace = systemConf["trace"].as(false); m_debug = systemConf["debug"].as(false); @@ -1044,6 +1068,7 @@ bool HostBridge::createNetwork() m_udpReceiveAddress = networkConf["udpReceiveAddress"].as(); m_udpUseULaw = networkConf["udpUseULaw"].as(false); m_udpUsrp = networkConf["udpUsrp"].as(false); + m_udpSilenceDuringHang = networkConf["udpHangSilence"].as(true); if (m_udpUsrp) { m_udpMetadata = false; // USRP disables metadata due to USRP always having metadata @@ -1063,6 +1088,9 @@ bool HostBridge::createNetwork() if (m_udpUseULaw && m_udpMetadata) m_udpMetadata = false; // metadata isn't supported when encoding uLaw + if (m_udpSilenceDuringHang && m_udpRTPFrames) + m_udpCallClock = Timer(1000U, 0U, 160U); // packets every 160ms + yaml::Node tekConf = networkConf["tek"]; bool tekEnable = tekConf["enable"].as(false); std::string tekAlgo = tekConf["tekAlgo"].as(); @@ -1160,6 +1188,7 @@ bool HostBridge::createNetwork() LogInfo(" UDP Audio RTP Framed: %s", m_udpRTPFrames ? "yes" : "no"); } LogInfo(" UDP Audio USRP: %s", m_udpUsrp ? "yes" : "no"); + LogInfo(" UDP Silence During Hangtime: %s", m_udpSilenceDuringHang ? "yes" : "no"); } LogInfo(" Traffic Encrypted: %s", tekEnable ? "yes" : "no"); @@ -1375,15 +1404,18 @@ void HostBridge::processUDPAudio() } } - m_dropTime.stop(); + m_udpCallClock.stop(); + m_udpDropTime.stop(); - if (!m_dropTime.isRunning()) - m_dropTime.start(); + if (!m_udpDropTime.isRunning()) + m_udpDropTime.start(); + m_udpCallClock.start(); } // If audio detection is active and no call is in progress, encode and transmit the audio if (m_audioDetect && !m_callInProgress) { - m_dropTime.start(); + m_udpDropTime.start(); + m_udpCallClock.start(); switch (m_txMode) { case TX_MODE_DMR: @@ -2708,7 +2740,7 @@ void HostBridge::callEnd(uint32_t srcId, uint32_t dstId) trafficType = UDP_CALL; } - if (srcId == 0U && !m_audioDetect && !m_dropTime.isRunning()) { + if (srcId == 0U && !m_audioDetect && (!m_localDropTime.isRunning() || !m_udpDropTime.isRunning())) { LogError(LOG_HOST, "%s, call end, ignoring invalid call end, srcId = %u, dstId = %u", trafficType.c_str(), srcId, dstId); return; } @@ -2716,7 +2748,10 @@ void HostBridge::callEnd(uint32_t srcId, uint32_t dstId) LogMessage(LOG_HOST, "%s, call end, srcId = %u, dstId = %u", trafficType.c_str(), srcId, dstId); m_audioDetect = false; - m_dropTime.stop(); + m_localDropTime.stop(); + m_udpDropTime.stop(); + m_udpHangTime.stop(); + m_udpCallClock.stop(); if (!m_callInProgress) { switch (m_txMode) { @@ -2911,17 +2946,17 @@ void* HostBridge::threadAudioProcess(void* arg) } } - bridge->m_dropTime.stop(); + bridge->m_localDropTime.stop(); } else { // if we've exceeded the audio drop timeout, then really drop the audio - if (bridge->m_dropTime.isRunning() && bridge->m_dropTime.hasExpired()) { + if (bridge->m_localDropTime.isRunning() && bridge->m_localDropTime.hasExpired()) { if (bridge->m_audioDetect) { bridge->callEnd(srcId, dstId); } } - if (!bridge->m_dropTime.isRunning()) - bridge->m_dropTime.start(); + if (!bridge->m_localDropTime.isRunning()) + bridge->m_localDropTime.start(); } if (bridge->m_audioDetect && !bridge->m_callInProgress) { @@ -3032,6 +3067,161 @@ void* HostBridge::threadNetworkProcess(void* arg) return nullptr; } +/* Helper to reset IMBE buffer with null frames. */ + +void HostBridge::resetWithNullAudio(uint8_t* data, bool encrypted) +{ + if (data == nullptr) + return; + + // clear buffer for next sequence + ::memset(data, 0x00U, 9U * 25U); + + // fill with null + if (!encrypted) { + ::memcpy(data + 10U, P25DEF::NULL_IMBE, 11U); + ::memcpy(data + 26U, P25DEF::NULL_IMBE, 11U); + ::memcpy(data + 55U, P25DEF::NULL_IMBE, 11U); + + ::memcpy(data + 80U, P25DEF::NULL_IMBE, 11U); + ::memcpy(data + 105U, P25DEF::NULL_IMBE, 11U); + ::memcpy(data + 130U, P25DEF::NULL_IMBE, 11U); + + ::memcpy(data + 155U, P25DEF::NULL_IMBE, 11U); + ::memcpy(data + 180U, P25DEF::NULL_IMBE, 11U); + ::memcpy(data + 204U, P25DEF::NULL_IMBE, 11U); + } + else { + ::memcpy(data + 10U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + ::memcpy(data + 26U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + ::memcpy(data + 55U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + + ::memcpy(data + 80U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + ::memcpy(data + 105U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + ::memcpy(data + 130U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + + ::memcpy(data + 155U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + ::memcpy(data + 180U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + ::memcpy(data + 204U, P25DEF::ENCRYPTED_NULL_IMBE, 11U); + } +} + +/* */ + +void HostBridge::callEndSilence(uint32_t srcId, uint32_t dstId) +{ + switch (m_txMode) { + case TX_MODE_DMR: + { + using namespace dmr; + using namespace dmr::defines; + + m_dmrN = (uint8_t)(m_dmrSeqNo % 6); + + // send DMR voice + uint8_t data[DMR_FRAME_LENGTH_BYTES]; + + m_ambeCount = 0U; + + ::memcpy(m_ambeBuffer + (m_ambeCount * 9U), NULL_AMBE, RAW_AMBE_LENGTH_BYTES); + m_ambeCount++; + ::memcpy(m_ambeBuffer + (m_ambeCount * 9U), NULL_AMBE, RAW_AMBE_LENGTH_BYTES); + m_ambeCount++; + ::memcpy(m_ambeBuffer + (m_ambeCount * 9U), NULL_AMBE, RAW_AMBE_LENGTH_BYTES); + m_ambeCount++; + + ::memcpy(data, m_ambeBuffer, 13U); + data[13U] = (uint8_t)(m_ambeBuffer[13U] & 0xF0); + data[19U] = (uint8_t)(m_ambeBuffer[13U] & 0x0F); + ::memcpy(data + 20U, m_ambeBuffer + 14U, 13U); + + DataType::E dataType = DataType::VOICE_SYNC; + if (m_dmrN == 0) + dataType = DataType::VOICE_SYNC; + else { + dataType = DataType::VOICE; + + uint8_t lcss = m_dmrEmbeddedData.getData(data, m_dmrN); + + // generated embedded signalling + data::EMB emb = data::EMB(); + emb.setColorCode(0U); + emb.setLCSS(lcss); + emb.encode(data); + } + + LogMessage(LOG_HOST, DMR_DT_VOICE ", silence srcId = %u, dstId = %u, slot = %u, seqNo = %u", srcId, dstId, m_slot, m_dmrN); + + // generate DMR network frame + data::NetData dmrData; + dmrData.setSlotNo(m_slot); + dmrData.setDataType(dataType); + dmrData.setSrcId(srcId); + dmrData.setDstId(dstId); + dmrData.setFLCO(FLCO::GROUP); + dmrData.setN(m_dmrN); + dmrData.setSeqNo(m_dmrSeqNo); + dmrData.setBER(0U); + dmrData.setRSSI(0U); + + dmrData.setData(data); + + m_network->writeDMR(dmrData, false); + m_txStreamId = m_network->getDMRStreamId(m_slot); + + m_dmrSeqNo++; + } + break; + case TX_MODE_P25: + { + using namespace p25; + using namespace p25::defines; + + // fill the LDU buffers appropriately + switch (m_p25N) { + // LDU1 + case 0: + resetWithNullAudio(m_netLDU1, false); + break; + + // LDU2 + case 1: + resetWithNullAudio(m_netLDU2, false); + break; + } + + lc::LC lc = lc::LC(); + lc.setLCO(LCO::GROUP); + lc.setGroup(true); + lc.setPriority(4U); + lc.setDstId(dstId); + lc.setSrcId(srcId); + + lc.setAlgId(ALGO_UNENCRYPT); + lc.setKId(0); + + data::LowSpeedData lsd = data::LowSpeedData(); + + // send P25 LDU1 + if (m_p25N == 0U) { + LogMessage(LOG_HOST, P25_LDU1_STR " silence audio, srcId = %u, dstId = %u", srcId, dstId); + m_network->writeP25LDU1(lc, lsd, m_netLDU1, FrameType::DATA_UNIT); + m_p25N++; + break; + } + + // send P25 LDU2 + if (m_p25N == 1U) { + LogMessage(LOG_HOST, P25_LDU2_STR " silence audio, algo = $%02X, kid = $%04X", ALGO_UNENCRYPT, 0U); + m_network->writeP25LDU2(lc, lsd, m_netLDU2); + m_p25N = 0U; + break; + } + } + break; + } +} + /* Entry point to call watchdog handler thread. */ void* HostBridge::threadCallWatchdog(void* arg) @@ -3073,8 +3263,14 @@ void* HostBridge::threadCallWatchdog(void* arg) uint32_t ms = stopWatch.elapsed(); stopWatch.start(); - if (bridge->m_dropTime.isRunning()) - bridge->m_dropTime.clock(ms); + if (!bridge->m_trafficFromUDP) { + if (bridge->m_localDropTime.isRunning()) + bridge->m_localDropTime.clock(ms); + } + else { + if (bridge->m_udpDropTime.isRunning()) + bridge->m_udpDropTime.clock(ms); + } std::string trafficType = LOCAL_CALL; if (bridge->m_trafficFromUDP) @@ -3093,15 +3289,47 @@ void* HostBridge::threadCallWatchdog(void* arg) srcId = bridge->m_udpSrcId; dstId = bridge->m_udpDstId; - if (bridge->m_dropTime.isRunning() && bridge->m_dropTime.hasExpired()) { - if (bridge->m_trafficFromUDP) { - bridge->callEnd(srcId, dstId); + if (bridge->m_udpSilenceDuringHang) { + if (bridge->m_udpCallClock.isRunning()) + bridge->m_udpCallClock.clock(ms); + + if (bridge->m_udpCallClock.isRunning() && bridge->m_udpCallClock.hasExpired()) { + bridge->m_udpCallClock.stop(); + bridge->m_udpHangTime.start(); + + bridge->m_dmrN = 0U; + bridge->m_p25N = 0U; + } + + if (bridge->m_udpHangTime.isRunning()) + bridge->m_udpHangTime.clock(ms); + + if (bridge->m_udpHangTime.isRunning() && bridge->m_udpHangTime.hasExpired()) { + bridge->callEndSilence(srcId, dstId); + bridge->m_udpHangTime.start(); } } + + if (bridge->m_udpDropTime.isRunning() && bridge->m_udpDropTime.hasExpired()) { + if (bridge->m_udpSilenceDuringHang) { + bridge->m_udpHangTime.stop(); + switch (bridge->m_txMode) { + case TX_MODE_DMR: + // TODO: send DMR silence + break; + case TX_MODE_P25: + if (bridge->m_p25N > 0U) + bridge->callEndSilence(srcId, dstId); + break; + } + } + + bridge->callEnd(srcId, dstId); + } } else { // if we've exceeded the drop timeout, then really drop the audio - if (bridge->m_dropTime.isRunning() && (bridge->m_dropTime.getTimer() >= dropTimeout)) { + if (bridge->m_localDropTime.isRunning() && (bridge->m_localDropTime.getTimer() >= dropTimeout)) { LogMessage(LOG_HOST, "%s, terminating stuck call", trafficType.c_str()); bridge->callEnd(srcId, dstId); } diff --git a/src/bridge/HostBridge.h b/src/bridge/HostBridge.h index d36f8531..bb35a522 100644 --- a/src/bridge/HostBridge.h +++ b/src/bridge/HostBridge.h @@ -166,6 +166,7 @@ private: bool m_udpUseULaw; bool m_udpRTPFrames; bool m_udpUsrp; + bool m_udpSilenceDuringHang; uint8_t m_tekAlgoId; uint16_t m_tekKeyId; @@ -192,7 +193,10 @@ private: float m_voxSampleLevel; uint16_t m_dropTimeMS; - Timer m_dropTime; + Timer m_localDropTime; + Timer m_udpCallClock; + Timer m_udpHangTime; + Timer m_udpDropTime; bool m_detectAnalogMDC1200; @@ -506,6 +510,20 @@ private: */ static void* threadNetworkProcess(void* arg); + /** + * @brief Helper to reset IMBE buffer with null frames. + * @param data Buffer containing frame data. + * @param encrypted Flag indicating whether or not the data is encrypted. + */ + void resetWithNullAudio(uint8_t* data, bool encrypted); + + /** + * @brief Helper to send silence audio frames. + * @param srcId + * @param dstId + */ + void callEndSilence(uint32_t srcId, uint32_t dstId); + /** * @brief Entry point to call watchdog handler thread. * @param arg Instance of the thread_t structure.