From 9e047e99218b8985752542445debc7f4e4a9ce91 Mon Sep 17 00:00:00 2001 From: firealarmss Date: Fri, 7 Mar 2025 07:09:45 -0600 Subject: [PATCH] Add initial support for DVM FNE --- .gitmodules | 3 + WhackerLinkConsoleV2.sln | 45 +- WhackerLinkConsoleV2/AmbeNative.cs | 444 ++++++ WhackerLinkConsoleV2/Audio/alert1.wav | Bin 0 -> 48208 bytes WhackerLinkConsoleV2/Audio/alert2.wav | Bin 0 -> 56108 bytes WhackerLinkConsoleV2/Audio/alert3.wav | Bin 0 -> 61428 bytes WhackerLinkConsoleV2/Audio/emergency.wav | Bin 0 -> 4948 bytes WhackerLinkConsoleV2/Audio/hold.wav | Bin 0 -> 8282 bytes WhackerLinkConsoleV2/ChannelBox.xaml.cs | 37 +- WhackerLinkConsoleV2/FneSystemBase.DMR.cs | 113 ++ WhackerLinkConsoleV2/FneSystemBase.NXDN.cs | 49 + WhackerLinkConsoleV2/FneSystemBase.P25.cs | 470 ++++++ WhackerLinkConsoleV2/FneSystemBase.cs | 193 +++ WhackerLinkConsoleV2/FneSystemManager.cs | 103 ++ WhackerLinkConsoleV2/MainWindow.xaml.cs | 1262 ++++++++++++++--- WhackerLinkConsoleV2/P25Crypto.cs | 205 +++ WhackerLinkConsoleV2/PeerSystem.cs | 115 ++ WhackerLinkConsoleV2/SampleTimeConvert.cs | 72 + WhackerLinkConsoleV2/VocoderInterop.cs | 397 ++++++ .../WhackerLinkConsoleV2.csproj | 23 +- fnecore | 1 + 21 files changed, 3304 insertions(+), 228 deletions(-) create mode 100644 WhackerLinkConsoleV2/AmbeNative.cs create mode 100644 WhackerLinkConsoleV2/Audio/alert1.wav create mode 100644 WhackerLinkConsoleV2/Audio/alert2.wav create mode 100644 WhackerLinkConsoleV2/Audio/alert3.wav create mode 100644 WhackerLinkConsoleV2/Audio/emergency.wav create mode 100644 WhackerLinkConsoleV2/Audio/hold.wav create mode 100644 WhackerLinkConsoleV2/FneSystemBase.DMR.cs create mode 100644 WhackerLinkConsoleV2/FneSystemBase.NXDN.cs create mode 100644 WhackerLinkConsoleV2/FneSystemBase.P25.cs create mode 100644 WhackerLinkConsoleV2/FneSystemBase.cs create mode 100644 WhackerLinkConsoleV2/FneSystemManager.cs create mode 100644 WhackerLinkConsoleV2/P25Crypto.cs create mode 100644 WhackerLinkConsoleV2/PeerSystem.cs create mode 100644 WhackerLinkConsoleV2/SampleTimeConvert.cs create mode 100644 WhackerLinkConsoleV2/VocoderInterop.cs create mode 160000 fnecore diff --git a/.gitmodules b/.gitmodules index 602833d..6d49c83 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "WhackerLinkLib"] path = WhackerLinkLib url = https://github.com/whackerlink/WhackerLinkLib +[submodule "fnecore"] + path = fnecore + url = https://github.com/dvmproject/fnecore diff --git a/WhackerLinkConsoleV2.sln b/WhackerLinkConsoleV2.sln index 0309ce5..b14cad8 100644 --- a/WhackerLinkConsoleV2.sln +++ b/WhackerLinkConsoleV2.sln @@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "fnecore", "fnecore\fnecore.csproj", "{1F06ECB1-9928-1430-63F4-2E01522A0510}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -23,6 +25,9 @@ Global Release|Any CPU = Release|Any CPU Release|x64 = Release|x64 Release|x86 = Release|x86 + WIN32|Any CPU = WIN32|Any CPU + WIN32|x64 = WIN32|x64 + WIN32|x86 = WIN32|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {5918329A-6374-40E2-874D-445360C89676}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -43,12 +48,18 @@ Global {5918329A-6374-40E2-874D-445360C89676}.Release|x64.Build.0 = Release|x64 {5918329A-6374-40E2-874D-445360C89676}.Release|x86.ActiveCfg = Release|x86 {5918329A-6374-40E2-874D-445360C89676}.Release|x86.Build.0 = Release|x86 + {5918329A-6374-40E2-874D-445360C89676}.WIN32|Any CPU.ActiveCfg = WIN32|Any CPU + {5918329A-6374-40E2-874D-445360C89676}.WIN32|Any CPU.Build.0 = WIN32|Any CPU + {5918329A-6374-40E2-874D-445360C89676}.WIN32|x64.ActiveCfg = WIN32|x64 + {5918329A-6374-40E2-874D-445360C89676}.WIN32|x64.Build.0 = WIN32|x64 + {5918329A-6374-40E2-874D-445360C89676}.WIN32|x86.ActiveCfg = WIN32|x86 + {5918329A-6374-40E2-874D-445360C89676}.WIN32|x86.Build.0 = WIN32|x86 {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|Any CPU.Build.0 = Debug|Any CPU {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|x64.ActiveCfg = Debug|x64 {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|x64.Build.0 = Debug|x64 - {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|x86.ActiveCfg = Debug|Any CPU - {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|x86.Build.0 = Debug|Any CPU + {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|x86.ActiveCfg = Debug|x86 + {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Debug|x86.Build.0 = Debug|x86 {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.NoVocode|Any CPU.ActiveCfg = Debug|Any CPU {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.NoVocode|Any CPU.Build.0 = Debug|Any CPU {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.NoVocode|x64.ActiveCfg = Debug|x64 @@ -61,6 +72,36 @@ Global {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x64.Build.0 = Release|x64 {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x86.ActiveCfg = Release|Any CPU {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x86.Build.0 = Release|Any CPU + {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|Any CPU.ActiveCfg = WIN32|Any CPU + {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|Any CPU.Build.0 = WIN32|Any CPU + {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|x64.ActiveCfg = WIN32|x64 + {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|x64.Build.0 = WIN32|x64 + {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|x86.ActiveCfg = WIN32|x86 + {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|x86.Build.0 = WIN32|x86 + {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|x64.ActiveCfg = Debug|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|x64.Build.0 = Debug|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|x86.ActiveCfg = Debug|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.Debug|x86.Build.0 = Debug|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|Any CPU.ActiveCfg = Release|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|Any CPU.Build.0 = Release|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|x64.ActiveCfg = Release|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|x64.Build.0 = Release|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|x86.ActiveCfg = Release|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.NoVocode|x86.Build.0 = Release|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|Any CPU.Build.0 = Release|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|x64.ActiveCfg = Release|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|x64.Build.0 = Release|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|x86.ActiveCfg = Release|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|x86.Build.0 = Release|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|Any CPU.ActiveCfg = WIN32|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|Any CPU.Build.0 = WIN32|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x64.ActiveCfg = WIN32|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x64.Build.0 = WIN32|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x86.ActiveCfg = WIN32|Any CPU + {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x86.Build.0 = WIN32|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/WhackerLinkConsoleV2/AmbeNative.cs b/WhackerLinkConsoleV2/AmbeNative.cs new file mode 100644 index 0000000..c7ba576 --- /dev/null +++ b/WhackerLinkConsoleV2/AmbeNative.cs @@ -0,0 +1,444 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +#if WIN32 +namespace WhackerLinkConsoleV2 +{ + /// + /// Implements P/Invoke to callback into external AMBE encoder/decoder library. + /// + /// This is used to interface to a external library that talks to a DVSI USB-3000. + public class AmbeVocoder + { + private const int MBE_SAMPLES_LENGTH = 160; + private const short NO_BIT_STEAL = 0; + + private const ushort ECMODE_NOISE_SUPPRESS = 0x40; + private const ushort ECMODE_AGC = 0x2000; + + private const int DECSTATE_SIZE = 2048; + private byte[] decoderState; + private const int ENCSTATE_SIZE = 6144; + private byte[] encoderState; + + /// + /// + /// + public enum AmbeMode : short + { + FULL_RATE = 0x00, + HALF_RATE = 0x01, + + NOT_VALID = 0x03 + } // public enum AmbeMode + + private AmbeMode mode; + private int frameLengthInBytes; + private int frameLengthInBits; + + private ushort dcmode; + private ushort ecmode; + + private MBEInterleaver interleaver; + + /* + ** Properties + */ + + /// + /// Gets the currently operating decoder mode. + /// + public AmbeMode DecoderMode + { + get + { + unsafe + { + fixed (byte* state = decoderState) + return (AmbeMode)ambe_get_dec_mode((IntPtr)state); + } + } + } + + /// + /// Gets the currently operating encoder mode. + /// + public AmbeMode EncoderMode + { + get + { + unsafe + { + fixed (byte* state = encoderState) + return (AmbeMode)ambe_get_enc_mode((IntPtr)state); + } + } + } + + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// + /// + public AmbeVocoder(bool fullRate = true) + { + if (fullRate) + { + this.mode = AmbeMode.FULL_RATE; + this.interleaver = new MBEInterleaver(MBE_MODE.IMBE_88BIT); + this.frameLengthInBits = 88; + this.frameLengthInBytes = 11; + } + else + { + this.mode = AmbeMode.HALF_RATE; + this.interleaver = new MBEInterleaver(MBE_MODE.DMR_AMBE); + this.frameLengthInBits = 49; + this.frameLengthInBytes = 7; + } + + this.decoderState = new byte[DECSTATE_SIZE]; + this.dcmode = 0; + + this.encoderState = new byte[ENCSTATE_SIZE]; + this.ecmode = ECMODE_NOISE_SUPPRESS | ECMODE_AGC; + + unsafe + { + // initialize the AMBE decoder state + fixed (byte* state = decoderState) + ambe_init_dec((IntPtr)state, (short)mode); + + // initialize the AMBE encoder state + fixed (byte* state = encoderState) + ambe_init_enc((IntPtr)state, (short)mode, 1); + } + } + + /// + /// Helper to unpack the codeword bytes into codeword bits for use with the AMBE decoder. + /// + /// output bits array. + /// Codeword bits. + /// Codeword bytes. + /// Length of codeword in bytes. + /// Length of codeword in bits. + private void unpackBytesToBits(out short[] codewordBits, byte[] codeword, int lengthBytes, int lengthBits) + { + codewordBits = new short[this.frameLengthInBits * 2]; + + int processed = 0, bitPtr = 0, bytePtr = 0; + for (int i = 0; i < lengthBytes; i++) + { + for (int j = 7; -1 < j; j--) + { + if (processed < lengthBits) + { + codewordBits[bitPtr] = (short)((codeword[bytePtr] >> ((byte)j & 0x1F)) & 1); + bitPtr++; + } + + processed++; + } + + bytePtr++; + } + } + + /// + /// Helper to unpack the codeword bytes into codeword bits for use with the AMBE decoder. + /// + /// output bits array. + /// Codeword bits. + /// Codeword bytes. + /// Length of codeword in bytes. + /// Length of codeword in bits. + private void unpackBytesToBits(out byte[] codewordBits, byte[] codeword, int lengthBytes, int lengthBits) + { + codewordBits = new byte[this.frameLengthInBits * 2]; + + int processed = 0, bitPtr = 0, bytePtr = 0; + for (int i = 0; i < lengthBytes; i++) + { + for (int j = 7; -1 < j; j--) + { + if (processed < lengthBits) + { + codewordBits[bitPtr] = (byte)((codeword[bytePtr] >> ((byte)j & 0x1F)) & 1); + bitPtr++; + } + + processed++; + } + + bytePtr++; + } + } + + /// + /// Decodes the given MBE codewords to PCM samples using the decoder mode. + /// + /// + /// + public int decode(byte[] codeword, out short[] samples) + { + samples = new short[MBE_SAMPLES_LENGTH]; + + if (codeword == null) + throw new NullReferenceException("codeword"); + + // is this a DMR codeword? + if (codeword.Length > frameLengthInBytes && mode == AmbeMode.HALF_RATE && + codeword.Length == 9) + { + // use the managed vocoder to retrieve the un-ECC'ed and uninterleaved AMBE bits + byte[] bits = new byte[49]; + interleaver.Decode(codeword, bits); + + // repack bits into 7-byte array + packBitsToBytes(bits, out codeword, frameLengthInBytes, frameLengthInBits); + } + + if (codeword.Length > frameLengthInBytes) + throw new ArgumentOutOfRangeException($"Codeword length is > {frameLengthInBytes}"); + + if (codeword.Length < frameLengthInBytes) + throw new ArgumentOutOfRangeException($"Codeword length is < {frameLengthInBytes}"); + + // unpack codeword from bytes to bits for use with external library + short[] codewordBits = null; + unpackBytesToBits(out codewordBits, codeword, frameLengthInBytes, frameLengthInBits); + + short[] n0 = new short[MBE_SAMPLES_LENGTH / 2]; + short[] n1 = new short[MBE_SAMPLES_LENGTH / 2]; + + // perform P/Invoke callback and pointer pinning and callback into external library + unsafe + { + fixed (short* c = codewordBits) + fixed (byte* state = decoderState) + { + IntPtr codewordPtr = (IntPtr)c; + + // sample segment 1 + GCHandle pinnedN0 = GCHandle.Alloc(n0, GCHandleType.Pinned); + IntPtr n0Ptr = pinnedN0.AddrOfPinnedObject(); + ambe_voice_dec(n0Ptr, MBE_SAMPLES_LENGTH / 2, codewordPtr, NO_BIT_STEAL, dcmode, 0, (IntPtr)state); + pinnedN0.Free(); + + // sample segment 2 + GCHandle pinnedN1 = GCHandle.Alloc(n1, GCHandleType.Pinned); + IntPtr n1Ptr = pinnedN1.AddrOfPinnedObject(); + ambe_voice_dec(n1Ptr, MBE_SAMPLES_LENGTH / 2, codewordPtr, NO_BIT_STEAL, dcmode, 1, (IntPtr)state); + pinnedN1.Free(); + } + } + + // combine sample segments into contiguous samples + for (int i = 0; i < MBE_SAMPLES_LENGTH / 2; i++) + samples[i] = n0[i]; + for (int i = 0; i < MBE_SAMPLES_LENGTH / 2; i++) + samples[i + (MBE_SAMPLES_LENGTH / 2)] = n1[i]; + + return 0; + } + + /// + /// Calls ambe_init_dec() in the external DLL. + /// + /// Buffer containing the decoder state to initialize. + /// AMBE mode; FULL (0) or HALF (1). + [DllImport("AMBE.DLL", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern void ambe_init_dec([Out] IntPtr state, [In] short mode); + + /// + /// Calls ambe_get_dec_mode() in the external DLL. + /// + /// Buffer containing the decoder state to initialize. + [DllImport("AMBE.DLL", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern short ambe_get_dec_mode([In] IntPtr state); + + /// + /// Calls ambe_voice_dec() in the external DLL. + /// + /// + /// + /// + /// + /// + /// + /// Buffer containing the decoder state. + /// + [DllImport("AMBE.DLL", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern uint ambe_voice_dec([Out] IntPtr samples, [In] short sampleLength, [In] IntPtr codeword, [In] short bitSteal, [In] ushort cmode, [In] short n, [In] IntPtr state); + + /// + /// Helper to pack the codeword bits into codeword bytes for use with the AMBE encoder. + /// + /// input bits array. + /// Codeword bits. + /// Codeword bytes. + /// Length of codeword in bytes. + /// Length of codeword in bits. + private void packBitsToBytes(short[] codewordBits, out byte[] codeword, int lengthBytes, int lengthBits) + { + codeword = new byte[lengthBytes]; + + int processed = 0, bitPtr = 0, bytePtr = 0; + for (int i = 0; i < lengthBytes; i++) + { + codeword[i] = 0; + for (int j = 7; -1 < j; j--) + { + if (processed < lengthBits) + { + codeword[bytePtr] = (byte)(codeword[bytePtr] | (byte)((codewordBits[bitPtr] & 1) << ((byte)j & 0x1F))); + bitPtr++; + } + + processed++; + } + + bytePtr++; + } + } + + /// + /// Helper to pack the codeword bits into codeword bytes for use with the AMBE encoder. + /// + /// input bits array. + /// Codeword bits. + /// Codeword bytes. + /// Length of codeword in bytes. + /// Length of codeword in bits. + private void packBitsToBytes(byte[] codewordBits, out byte[] codeword, int lengthBytes, int lengthBits) + { + codeword = new byte[lengthBytes]; + + int processed = 0, bitPtr = 0, bytePtr = 0; + for (int i = 0; i < lengthBytes; i++) + { + codeword[i] = 0; + for (int j = 7; -1 < j; j--) + { + if (processed < lengthBits) + { + codeword[bytePtr] = (byte)(codeword[bytePtr] | (byte)((codewordBits[bitPtr] & 1) << ((byte)j & 0x1F))); + bitPtr++; + } + + processed++; + } + + bytePtr++; + } + } + + /// + /// Encodes the given PCM samples using the encoder mode to MBE codewords. + /// + /// + /// + /// + public void encode(short[] samples, out byte[] codeword, bool encodeDMR = false) + { + codeword = new byte[this.frameLengthInBytes]; + + if (samples == null) + throw new NullReferenceException("samples"); + + if (samples.Length > MBE_SAMPLES_LENGTH) + throw new ArgumentOutOfRangeException($"Samples length is > {MBE_SAMPLES_LENGTH}"); + if (samples.Length < MBE_SAMPLES_LENGTH) + throw new ArgumentOutOfRangeException($"Samples length is < {MBE_SAMPLES_LENGTH}"); + + short[] codewordBits = new short[this.frameLengthInBits * 2]; + + // split samples into 2 segments + short[] n0 = new short[MBE_SAMPLES_LENGTH / 2]; + for (int i = 0; i < MBE_SAMPLES_LENGTH / 2; i++) + n0[i] = samples[i]; + + short[] n1 = new short[MBE_SAMPLES_LENGTH / 2]; + for (int i = 0; i < MBE_SAMPLES_LENGTH / 2; i++) + n1[i] = samples[i + (MBE_SAMPLES_LENGTH / 2)]; + + // perform P/Invoke callback and pointer pinning and callback into external library + unsafe + { + fixed (short* c = codewordBits) + fixed (byte* state = encoderState) + { + IntPtr codewordPtr = (IntPtr)c; + + // sample segment 1 + GCHandle pinnedN0 = GCHandle.Alloc(n0, GCHandleType.Pinned); + IntPtr n0Ptr = pinnedN0.AddrOfPinnedObject(); + ambe_voice_enc(codewordPtr, NO_BIT_STEAL, n0Ptr, MBE_SAMPLES_LENGTH / 2, ecmode, 0, 8192, (IntPtr)state); + pinnedN0.Free(); + + // sample segment 2 + GCHandle pinnedN1 = GCHandle.Alloc(n1, GCHandleType.Pinned); + IntPtr n1Ptr = pinnedN1.AddrOfPinnedObject(); + ambe_voice_enc(codewordPtr, NO_BIT_STEAL, n1Ptr, MBE_SAMPLES_LENGTH / 2, ecmode, 1, 8192, (IntPtr)state); + pinnedN1.Free(); + } + } + + // is this to be a DMR codeword? + if (mode == AmbeMode.HALF_RATE && encodeDMR) + { + byte[] bits = new byte[49]; + for (int i = 0; i < 49; i++) + bits[i] = (byte)codewordBits[i]; + + // use the managed vocoder to create the ECC'ed and interleaved AMBE bits + interleaver.Encode(bits, codeword); + } + else + { + // pack codeword from bits to bytes for use with external library + packBitsToBytes(codewordBits, out codeword, frameLengthInBytes, frameLengthInBits); + } + } + + /// + /// Calls ambe_init_enc() in the external DLL. + /// + /// Buffer containing the encoder state to initialize. + /// AMBE mode; FULL (0) or HALF (1). + /// Flag to initialize encoder state fully, 1 to initialize, 0 to not. + [DllImport("AMBE.DLL", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern void ambe_init_enc([Out] IntPtr state, [In] short mode, [In] short initialize); + + /// + /// Calls ambe_get_enc_mode() in the external DLL. + /// + /// Buffer containing the encoder state to initialize. + [DllImport("AMBE.DLL", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern short ambe_get_enc_mode([In] IntPtr state); + + /// + /// Calls ambe_voice_enc() in the external DLL. + /// + /// + /// + /// + /// + /// + /// + /// + /// Buffer containing the encoder state. + /// + [DllImport("AMBE.DLL", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern uint ambe_voice_enc([Out] IntPtr codeword, [In] short bitSteal, [In] IntPtr samples, [In] short sampleLength, [In] ushort cmode, [In] short n, [In] short unk, [In] IntPtr state); + } // public class AmbeVocoder +} +#endif \ No newline at end of file diff --git a/WhackerLinkConsoleV2/Audio/alert1.wav b/WhackerLinkConsoleV2/Audio/alert1.wav new file mode 100644 index 0000000000000000000000000000000000000000..e3a133b39f2e0872453db9d2546c3349ed446edd GIT binary patch literal 48208 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 P(YD(eB_#ykmKpQ^<-luK literal 0 HcmV?d00001 diff --git a/WhackerLinkConsoleV2/Audio/emergency.wav b/WhackerLinkConsoleV2/Audio/emergency.wav new file mode 100644 index 0000000000000000000000000000000000000000..8a63ed8cf14dc944a144a9a75e2e4a2d58be0b28 GIT binary patch literal 4948 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 DoMPT< literal 0 HcmV?d00001 diff --git a/WhackerLinkConsoleV2/Audio/hold.wav b/WhackerLinkConsoleV2/Audio/hold.wav new file mode 100644 index 0000000000000000000000000000000000000000..db50832c13edaf6aeadd7a96c2f62b45038519ad GIT binary patch literal 8282 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 literal 0 HcmV?d00001 diff --git a/WhackerLinkConsoleV2/ChannelBox.xaml.cs b/WhackerLinkConsoleV2/ChannelBox.xaml.cs index cfa1592..b805066 100644 --- a/WhackerLinkConsoleV2/ChannelBox.xaml.cs +++ b/WhackerLinkConsoleV2/ChannelBox.xaml.cs @@ -14,15 +14,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . * -* Copyright (C) 2024 Caleb, K4PHP +* Copyright (C) 2024-2025 Caleb, K4PHP * */ using System.ComponentModel; +using System.Security.Cryptography; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; +using fnecore.P25; namespace WhackerLinkConsoleV2.Controls { @@ -50,10 +52,31 @@ namespace WhackerLinkConsoleV2.Controls public event PropertyChangedEventHandler PropertyChanged; + public byte[] netLDU1 = new byte[9 * 25]; + public byte[] netLDU2 = new byte[9 * 25]; + + public int p25N { get; set; } = 0; + public int p25SeqNo { get; set; } = 0; + + public byte[] mi = new byte[P25Defines.P25_MI_LENGTH]; // Message Indicator + public byte algId = 0; // Algorithm ID + public ushort kId = 0; // Key ID + + public List chunkedPcm = new List(); + public string ChannelName { get; set; } public string SystemName { get; set; } public string DstId { get; set; } +#if WIN32 + public AmbeVocoder extFullRateVocoder; + public AmbeVocoder extHalfRateVocoder; +#endif + public MBEEncoder encoder; + public MBEDecoder decoder; + + public P25Crypto crypter = new P25Crypto(); + public bool IsReceiving { get; set; } = false; public string LastSrcId @@ -145,6 +168,8 @@ namespace WhackerLinkConsoleV2.Controls } } + public uint txStreamId { get; internal set; } + public ChannelBox(SelectedChannelsManager selectedChannelsManager, AudioManager audioManager, string channelName, string systemName, string dstId) { InitializeComponent(); @@ -196,6 +221,10 @@ namespace WhackerLinkConsoleV2.Controls PageSelectButton.IsEnabled = false; ChannelMarkerBtn.IsEnabled = false; } + + byte[] key = { 00, 00, 00, 00, 00 }; + + crypter.AddKey(00, 0xaa, key); } private void ChannelBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) @@ -256,11 +285,15 @@ namespace WhackerLinkConsoleV2.Controls Background = IsSelected ? (Brush)new BrushConverter().ConvertFrom("#FF0B004B") : Brushes.DarkGray; } - private void PTTButton_Click(object sender, RoutedEventArgs e) + private async void PTTButton_Click(object sender, RoutedEventArgs e) { if (!IsSelected) return; + if (PttState) + await Task.Delay(500); + PttState = !PttState; + PTTButtonClicked.Invoke(sender, this); } diff --git a/WhackerLinkConsoleV2/FneSystemBase.DMR.cs b/WhackerLinkConsoleV2/FneSystemBase.DMR.cs new file mode 100644 index 0000000..00f5da6 --- /dev/null +++ b/WhackerLinkConsoleV2/FneSystemBase.DMR.cs @@ -0,0 +1,113 @@ +using Microsoft.VisualBasic; +using Serilog; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Net; +using System.Text; +using System.Threading.Tasks; + +using fnecore.DMR; +using fnecore; + +using NAudio.Wave; + +namespace WhackerLinkConsoleV2 +{ + /// + /// Implements a FNE system base. + /// + public abstract partial class FneSystemBase : fnecore.FneSystemBase + { + public const int AMBE_BUF_LEN = 9; + public const int DMR_AMBE_LENGTH_BYTES = 27; + public const int AMBE_PER_SLOT = 3; + + public const int P25_FIXED_SLOT = 2; + + public const int SAMPLE_RATE = 8000; + public const int BITS_PER_SECOND = 16; + + public const int MBE_SAMPLES_LENGTH = 160; + + public const int AUDIO_BUFFER_MS = 20; + public const int AUDIO_NO_BUFFERS = 2; + public const int AFSK_AUDIO_BUFFER_MS = 60; + public const int AFSK_AUDIO_NO_BUFFERS = 4; + + public new const int DMR_PACKET_SIZE = 55; + public new const int DMR_FRAME_LENGTH_BYTES = 33; + + /* + ** Methods + */ + + /// + /// Callback used to validate incoming DMR data. + /// + /// Peer ID + /// Source Address + /// Destination Address + /// Slot Number + /// Call Type (Group or Private) + /// Frame Type + /// DMR Data Type + /// Stream ID + /// Raw message data + /// True, if data stream is valid, otherwise false. + protected override bool DMRDataValidate(uint peerId, uint srcId, uint dstId, byte slot, fnecore.CallType callType, FrameType frameType, DMRDataType dataType, uint streamId, byte[] message) + { + return true; + } + + /// + /// Creates an DMR frame message. + /// + /// + /// + /// + /// + /// + /// + /// + public void CreateDMRMessage(ref byte[] data, uint srcId, uint dstId, byte slot, FrameType frameType, byte seqNo, byte n) + { + RemoteCallData callData = new RemoteCallData() + { + SrcId = srcId, + DstId = dstId, + FrameType = frameType, + Slot = slot, + }; + + CreateDMRMessage(ref data, callData, seqNo, n); + } + + /// + /// Helper to send a DMR terminator with LC message. + /// + public void SendDMRTerminator(uint srcId, uint dstId, byte slot, int dmrSeqNo, byte dmrN, EmbeddedData embeddedData) + { + RemoteCallData callData = new RemoteCallData() + { + SrcId = srcId, + DstId = dstId, + FrameType = FrameType.DATA_SYNC, + Slot = slot + }; + + SendDMRTerminator(callData, ref dmrSeqNo, ref dmrN, embeddedData); + } + + /// + /// Event handler used to process incoming DMR data. + /// + /// + /// + protected override void DMRDataReceived(object sender, DMRDataReceivedEvent e) + { + return; + } + } // public abstract partial class FneSystemBase : fnecore.FneSystemBase +} \ No newline at end of file diff --git a/WhackerLinkConsoleV2/FneSystemBase.NXDN.cs b/WhackerLinkConsoleV2/FneSystemBase.NXDN.cs new file mode 100644 index 0000000..9cc7c86 --- /dev/null +++ b/WhackerLinkConsoleV2/FneSystemBase.NXDN.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using fnecore.NXDN; +using fnecore; + +namespace WhackerLinkConsoleV2 +{ + /// + /// Implements a FNE system base. + /// + public abstract partial class FneSystemBase : fnecore.FneSystemBase + { + private List> nxdnCallData = new List>(); + + /* + ** Methods + */ + + /// + /// Callback used to validate incoming NXDN data. + /// + /// Peer ID + /// Source Address + /// Destination Address + /// Call Type (Group or Private) + /// NXDN Message Type + /// Frame Type + /// Stream ID + /// Raw message data + /// True, if data stream is valid, otherwise false. + protected override bool NXDNDataValidate(uint peerId, uint srcId, uint dstId, CallType callType, NXDNMessageType messageType, FrameType frameType, uint streamId, byte[] message) + { + return true; + } + + /// + /// Event handler used to process incoming NXDN data. + /// + /// + /// + protected override void NXDNDataReceived(object sender, NXDNDataReceivedEvent e) + { + return; + } + } // public abstract partial class FneSystemBase : fnecore.FneSystemBase +} diff --git a/WhackerLinkConsoleV2/FneSystemBase.P25.cs b/WhackerLinkConsoleV2/FneSystemBase.P25.cs new file mode 100644 index 0000000..5bca64f --- /dev/null +++ b/WhackerLinkConsoleV2/FneSystemBase.P25.cs @@ -0,0 +1,470 @@ +// SPDX-License-Identifier: AGPL-3.0-only +/** +* Digital Voice Modem - Audio Bridge +* AGPLv3 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Audio Bridge +* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) +* +* Copyright (C) 2022-2024 Bryan Biedenkapp, N2PLL +* +*/ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using System.Threading.Tasks; + +using Serilog; + +using fnecore; +using fnecore.P25; + +using NAudio.Wave; +using System.Windows.Threading; + +namespace WhackerLinkConsoleV2 +{ + /// + /// Implements a FNE system base. + /// + public abstract partial class FneSystemBase : fnecore.FneSystemBase + { + public const int IMBE_BUF_LEN = 11; + + /* + ** Methods + */ + + /// + /// Callback used to validate incoming P25 data. + /// + /// Peer ID + /// Source Address + /// Destination Address + /// Call Type (Group or Private) + /// P25 DUID + /// Frame Type + /// Stream ID + /// Raw message data + /// True, if data stream is valid, otherwise false. + protected override bool P25DataValidate(uint peerId, uint srcId, uint dstId, CallType callType, P25DUID duid, FrameType frameType, uint streamId, byte[] message) + { + return true; + } + + /// + /// Event handler used to pre-process incoming P25 data. + /// + /// + /// + protected override void P25DataPreprocess(object sender, P25DataReceivedEvent e) + { + return; + } + + public void CreateNewP25MessageHdr(byte duid, RemoteCallData callData, ref byte[] data) + { + CreateP25MessageHdr(duid, callData, ref data); + } + + /// + /// Helper to send a P25 TDU message. + /// + /// + public void SendP25TDU(uint srcId, uint dstId, bool grantDemand = false) + { + RemoteCallData callData = new RemoteCallData() + { + SrcId = srcId, + DstId = dstId, + LCO = P25Defines.LC_GROUP + }; + + SendP25TDU(callData, grantDemand); + } + + /// + /// Encode a logical link data unit 1. + /// + /// + /// + /// + /// + /// + private void EncodeLDU1(ref byte[] data, int offset, byte[] imbe, byte frameType, uint srcId, uint dstId) + { + if (data == null) + throw new ArgumentNullException("data"); + if (imbe == null) + throw new ArgumentNullException("imbe"); + + // determine the LDU1 DFSI frame length, its variable + uint frameLength = P25DFSI.P25_DFSI_LDU1_VOICE1_FRAME_LENGTH_BYTES; + switch (frameType) + { + case P25DFSI.P25_DFSI_LDU1_VOICE1: + frameLength = P25DFSI.P25_DFSI_LDU1_VOICE1_FRAME_LENGTH_BYTES; + break; + case P25DFSI.P25_DFSI_LDU1_VOICE2: + frameLength = P25DFSI.P25_DFSI_LDU1_VOICE2_FRAME_LENGTH_BYTES; + break; + case P25DFSI.P25_DFSI_LDU1_VOICE3: + frameLength = P25DFSI.P25_DFSI_LDU1_VOICE3_FRAME_LENGTH_BYTES; + break; + case P25DFSI.P25_DFSI_LDU1_VOICE4: + frameLength = P25DFSI.P25_DFSI_LDU1_VOICE4_FRAME_LENGTH_BYTES; + break; + case P25DFSI.P25_DFSI_LDU1_VOICE5: + frameLength = P25DFSI.P25_DFSI_LDU1_VOICE5_FRAME_LENGTH_BYTES; + break; + case P25DFSI.P25_DFSI_LDU1_VOICE6: + frameLength = P25DFSI.P25_DFSI_LDU1_VOICE6_FRAME_LENGTH_BYTES; + break; + case P25DFSI.P25_DFSI_LDU1_VOICE7: + frameLength = P25DFSI.P25_DFSI_LDU1_VOICE7_FRAME_LENGTH_BYTES; + break; + case P25DFSI.P25_DFSI_LDU1_VOICE8: + frameLength = P25DFSI.P25_DFSI_LDU1_VOICE8_FRAME_LENGTH_BYTES; + break; + case P25DFSI.P25_DFSI_LDU1_VOICE9: + frameLength = P25DFSI.P25_DFSI_LDU1_VOICE9_FRAME_LENGTH_BYTES; + break; + default: + return; + } + + byte[] dfsiFrame = new byte[frameLength]; + + dfsiFrame[0U] = frameType; // Frame Type + + // different frame types mean different things + switch (frameType) + { + case P25DFSI.P25_DFSI_LDU1_VOICE2: + { + Buffer.BlockCopy(imbe, 0, dfsiFrame, 1, IMBE_BUF_LEN); // IMBE + } + break; + case P25DFSI.P25_DFSI_LDU1_VOICE3: + { + dfsiFrame[1U] = P25Defines.LC_GROUP; // LCO + dfsiFrame[2U] = 0; // MFId + dfsiFrame[3U] = 0; // Service Options + Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE + } + break; + case P25DFSI.P25_DFSI_LDU1_VOICE4: + { + dfsiFrame[1U] = (byte)((dstId >> 16) & 0xFFU); // Talkgroup Address + dfsiFrame[2U] = (byte)((dstId >> 8) & 0xFFU); + dfsiFrame[3U] = (byte)((dstId >> 0) & 0xFFU); + Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE + } + break; + case P25DFSI.P25_DFSI_LDU1_VOICE5: + { + dfsiFrame[1U] = (byte)((srcId >> 16) & 0xFFU); // Source Address + dfsiFrame[2U] = (byte)((srcId >> 8) & 0xFFU); + dfsiFrame[3U] = (byte)((srcId >> 0) & 0xFFU); + Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE + } + break; + case P25DFSI.P25_DFSI_LDU1_VOICE6: + { + dfsiFrame[1U] = 0; // RS (24,12,13) + dfsiFrame[2U] = 0; // RS (24,12,13) + dfsiFrame[3U] = 0; // RS (24,12,13) + Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE + } + break; + case P25DFSI.P25_DFSI_LDU1_VOICE7: + { + dfsiFrame[1U] = 0; // RS (24,12,13) + dfsiFrame[2U] = 0; // RS (24,12,13) + dfsiFrame[3U] = 0; // RS (24,12,13) + Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE + } + break; + case P25DFSI.P25_DFSI_LDU1_VOICE8: + { + dfsiFrame[1U] = 0; // RS (24,12,13) + dfsiFrame[2U] = 0; // RS (24,12,13) + dfsiFrame[3U] = 0; // RS (24,12,13) + Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE + } + break; + case P25DFSI.P25_DFSI_LDU1_VOICE9: + { + dfsiFrame[1U] = 0; // LSD MSB + dfsiFrame[2U] = 0; // LSD LSB + Buffer.BlockCopy(imbe, 0, dfsiFrame, 4, IMBE_BUF_LEN); // IMBE + } + break; + + case P25DFSI.P25_DFSI_LDU1_VOICE1: + default: + { + dfsiFrame[6U] = 0; // RSSI + Buffer.BlockCopy(imbe, 0, dfsiFrame, 10, IMBE_BUF_LEN); // IMBE + } + break; + } + + Buffer.BlockCopy(dfsiFrame, 0, data, offset, (int)frameLength); + } + + /// + /// Creates an P25 LDU1 frame message. + /// + /// + /// + public void CreateP25LDU1Message(in byte[] netLDU1, ref byte[] data, uint srcId, uint dstId) + { + // pack DFSI data + int count = P25_MSG_HDR_SIZE; + byte[] imbe = new byte[IMBE_BUF_LEN]; + + Buffer.BlockCopy(netLDU1, 10, imbe, 0, IMBE_BUF_LEN); + EncodeLDU1(ref data, 24, imbe, P25DFSI.P25_DFSI_LDU1_VOICE1, srcId, dstId); + count += (int)P25DFSI.P25_DFSI_LDU1_VOICE1_FRAME_LENGTH_BYTES; + + Buffer.BlockCopy(netLDU1, 26, imbe, 0, IMBE_BUF_LEN); + EncodeLDU1(ref data, 46, imbe, P25DFSI.P25_DFSI_LDU1_VOICE2, srcId, dstId); + count += (int)P25DFSI.P25_DFSI_LDU1_VOICE2_FRAME_LENGTH_BYTES; + + Buffer.BlockCopy(netLDU1, 55, imbe, 0, IMBE_BUF_LEN); + EncodeLDU1(ref data, 60, imbe, P25DFSI.P25_DFSI_LDU1_VOICE3, srcId, dstId); + count += (int)P25DFSI.P25_DFSI_LDU1_VOICE3_FRAME_LENGTH_BYTES; + + Buffer.BlockCopy(netLDU1, 80, imbe, 0, IMBE_BUF_LEN); + EncodeLDU1(ref data, 77, imbe, P25DFSI.P25_DFSI_LDU1_VOICE4, srcId, dstId); + count += (int)P25DFSI.P25_DFSI_LDU1_VOICE4_FRAME_LENGTH_BYTES; + + Buffer.BlockCopy(netLDU1, 105, imbe, 0, IMBE_BUF_LEN); + EncodeLDU1(ref data, 94, imbe, P25DFSI.P25_DFSI_LDU1_VOICE5, srcId, dstId); + count += (int)P25DFSI.P25_DFSI_LDU1_VOICE5_FRAME_LENGTH_BYTES; + + Buffer.BlockCopy(netLDU1, 130, imbe, 0, IMBE_BUF_LEN); + EncodeLDU1(ref data, 111, imbe, P25DFSI.P25_DFSI_LDU1_VOICE6, srcId, dstId); + count += (int)P25DFSI.P25_DFSI_LDU1_VOICE6_FRAME_LENGTH_BYTES; + + Buffer.BlockCopy(netLDU1, 155, imbe, 0, IMBE_BUF_LEN); + EncodeLDU1(ref data, 128, imbe, P25DFSI.P25_DFSI_LDU1_VOICE7, srcId, dstId); + count += (int)P25DFSI.P25_DFSI_LDU1_VOICE7_FRAME_LENGTH_BYTES; + + Buffer.BlockCopy(netLDU1, 180, imbe, 0, IMBE_BUF_LEN); + EncodeLDU1(ref data, 145, imbe, P25DFSI.P25_DFSI_LDU1_VOICE8, srcId, dstId); + count += (int)P25DFSI.P25_DFSI_LDU1_VOICE8_FRAME_LENGTH_BYTES; + + Buffer.BlockCopy(netLDU1, 204, imbe, 0, IMBE_BUF_LEN); + EncodeLDU1(ref data, 162, imbe, P25DFSI.P25_DFSI_LDU1_VOICE9, srcId, dstId); + count += (int)P25DFSI.P25_DFSI_LDU1_VOICE9_FRAME_LENGTH_BYTES; + + data[23U] = (byte)count; + } + + /// + /// Encode a logical link data unit 2. + /// + /// + /// + /// + /// + private void EncodeLDU2(ref byte[] data, int offset, byte[] imbe, byte frameType) + { + if (data == null) + throw new ArgumentNullException("data"); + if (imbe == null) + throw new ArgumentNullException("imbe"); + + // determine the LDU2 DFSI frame length, its variable + uint frameLength = P25DFSI.P25_DFSI_LDU2_VOICE10_FRAME_LENGTH_BYTES; + switch (frameType) + { + case P25DFSI.P25_DFSI_LDU2_VOICE10: + frameLength = P25DFSI.P25_DFSI_LDU2_VOICE10_FRAME_LENGTH_BYTES; + break; + case P25DFSI.P25_DFSI_LDU2_VOICE11: + frameLength = P25DFSI.P25_DFSI_LDU2_VOICE11_FRAME_LENGTH_BYTES; + break; + case P25DFSI.P25_DFSI_LDU2_VOICE12: + frameLength = P25DFSI.P25_DFSI_LDU2_VOICE12_FRAME_LENGTH_BYTES; + break; + case P25DFSI.P25_DFSI_LDU2_VOICE13: + frameLength = P25DFSI.P25_DFSI_LDU2_VOICE13_FRAME_LENGTH_BYTES; + break; + case P25DFSI.P25_DFSI_LDU2_VOICE14: + frameLength = P25DFSI.P25_DFSI_LDU2_VOICE14_FRAME_LENGTH_BYTES; + break; + case P25DFSI.P25_DFSI_LDU2_VOICE15: + frameLength = P25DFSI.P25_DFSI_LDU2_VOICE15_FRAME_LENGTH_BYTES; + break; + case P25DFSI.P25_DFSI_LDU2_VOICE16: + frameLength = P25DFSI.P25_DFSI_LDU2_VOICE16_FRAME_LENGTH_BYTES; + break; + case P25DFSI.P25_DFSI_LDU2_VOICE17: + frameLength = P25DFSI.P25_DFSI_LDU2_VOICE17_FRAME_LENGTH_BYTES; + break; + case P25DFSI.P25_DFSI_LDU2_VOICE18: + frameLength = P25DFSI.P25_DFSI_LDU2_VOICE18_FRAME_LENGTH_BYTES; + break; + default: + return; + } + + byte[] dfsiFrame = new byte[frameLength]; + + dfsiFrame[0U] = frameType; // Frame Type + + // different frame types mean different things + switch (frameType) + { + case P25DFSI.P25_DFSI_LDU2_VOICE11: + { + Buffer.BlockCopy(imbe, 0, dfsiFrame, 1, IMBE_BUF_LEN); // IMBE + } + break; + case P25DFSI.P25_DFSI_LDU2_VOICE12: + { + dfsiFrame[1U] = 0; // Message Indicator + dfsiFrame[2U] = 0; + dfsiFrame[3U] = 0; + Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE + } + break; + case P25DFSI.P25_DFSI_LDU2_VOICE13: + { + dfsiFrame[1U] = 0; // Message Indicator + dfsiFrame[2U] = 0; + dfsiFrame[3U] = 0; + Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE + } + break; + case P25DFSI.P25_DFSI_LDU2_VOICE14: + { + dfsiFrame[1U] = 0; // Message Indicator + dfsiFrame[2U] = 0; + dfsiFrame[3U] = 0; + Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE + } + break; + case P25DFSI.P25_DFSI_LDU2_VOICE15: + { + dfsiFrame[1U] = P25Defines.P25_ALGO_UNENCRYPT; // Algorithm ID + dfsiFrame[2U] = 0; // Key ID + dfsiFrame[3U] = 0; + Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE + } + break; + case P25DFSI.P25_DFSI_LDU2_VOICE16: + { + // first 3 bytes of frame are supposed to be + // part of the RS(24, 16, 9) of the VOICE12, 13, 14, 15 + // control bytes + Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE + } + break; + case P25DFSI.P25_DFSI_LDU2_VOICE17: + { + // first 3 bytes of frame are supposed to be + // part of the RS(24, 16, 9) of the VOICE12, 13, 14, 15 + // control bytes + Buffer.BlockCopy(imbe, 0, dfsiFrame, 5, IMBE_BUF_LEN); // IMBE + } + break; + case P25DFSI.P25_DFSI_LDU2_VOICE18: + { + dfsiFrame[1U] = 0; // LSD MSB + dfsiFrame[2U] = 0; // LSD LSB + Buffer.BlockCopy(imbe, 0, dfsiFrame, 4, IMBE_BUF_LEN); // IMBE + } + break; + + case P25DFSI.P25_DFSI_LDU2_VOICE10: + default: + { + dfsiFrame[6U] = 0; // RSSI + Buffer.BlockCopy(imbe, 0, dfsiFrame, 10, IMBE_BUF_LEN); // IMBE + } + break; + } + + Buffer.BlockCopy(dfsiFrame, 0, data, offset, (int)frameLength); + } + + /// + /// Creates an P25 LDU2 frame message. + /// + /// Input LDU data array + /// Output data array + public void CreateP25LDU2Message(in byte[] netLDU2, ref byte[] data) + { + // pack DFSI data + int count = P25_MSG_HDR_SIZE; + byte[] imbe = new byte[IMBE_BUF_LEN]; + + Buffer.BlockCopy(netLDU2, 10, imbe, 0, IMBE_BUF_LEN); + EncodeLDU2(ref data, 24, imbe, P25DFSI.P25_DFSI_LDU2_VOICE10); + count += (int)P25DFSI.P25_DFSI_LDU2_VOICE10_FRAME_LENGTH_BYTES; + + Buffer.BlockCopy(netLDU2, 26, imbe, 0, IMBE_BUF_LEN); + EncodeLDU2(ref data, 46, imbe, P25DFSI.P25_DFSI_LDU2_VOICE11); + count += (int)P25DFSI.P25_DFSI_LDU2_VOICE11_FRAME_LENGTH_BYTES; + + Buffer.BlockCopy(netLDU2, 55, imbe, 0, IMBE_BUF_LEN); + EncodeLDU2(ref data, 60, imbe, P25DFSI.P25_DFSI_LDU2_VOICE12); + count += (int)P25DFSI.P25_DFSI_LDU2_VOICE12_FRAME_LENGTH_BYTES; + + Buffer.BlockCopy(netLDU2, 80, imbe, 0, IMBE_BUF_LEN); + EncodeLDU2(ref data, 77, imbe, P25DFSI.P25_DFSI_LDU2_VOICE13); + count += (int)P25DFSI.P25_DFSI_LDU2_VOICE13_FRAME_LENGTH_BYTES; + + Buffer.BlockCopy(netLDU2, 105, imbe, 0, IMBE_BUF_LEN); + EncodeLDU2(ref data, 94, imbe, P25DFSI.P25_DFSI_LDU2_VOICE14); + count += (int)P25DFSI.P25_DFSI_LDU2_VOICE14_FRAME_LENGTH_BYTES; + + Buffer.BlockCopy(netLDU2, 130, imbe, 0, IMBE_BUF_LEN); + EncodeLDU2(ref data, 111, imbe, P25DFSI.P25_DFSI_LDU2_VOICE15); + count += (int)P25DFSI.P25_DFSI_LDU2_VOICE15_FRAME_LENGTH_BYTES; + + Buffer.BlockCopy(netLDU2, 155, imbe, 0, IMBE_BUF_LEN); + EncodeLDU2(ref data, 128, imbe, P25DFSI.P25_DFSI_LDU2_VOICE16); + count += (int)P25DFSI.P25_DFSI_LDU2_VOICE16_FRAME_LENGTH_BYTES; + + Buffer.BlockCopy(netLDU2, 180, imbe, 0, IMBE_BUF_LEN); + EncodeLDU2(ref data, 145, imbe, P25DFSI.P25_DFSI_LDU2_VOICE17); + count += (int)P25DFSI.P25_DFSI_LDU2_VOICE17_FRAME_LENGTH_BYTES; + + Buffer.BlockCopy(netLDU2, 204, imbe, 0, IMBE_BUF_LEN); + EncodeLDU2(ref data, 162, imbe, P25DFSI.P25_DFSI_LDU2_VOICE18); + count += (int)P25DFSI.P25_DFSI_LDU2_VOICE18_FRAME_LENGTH_BYTES; + + data[23U] = (byte)count; + } + + /// + /// Event handler used to process incoming P25 data. + /// + /// + /// + protected override void P25DataReceived(object sender, P25DataReceivedEvent e) + { + DateTime pktTime = DateTime.Now; + + if ( e.DUID == P25DUID.TSDU || e.DUID == P25DUID.PDU) + return; + + byte control = e.Data[14U]; + + if (e.CallType == CallType.GROUP) + { + if (e.SrcId == 0) + return; + + mainWindow.P25DataReceived(e, pktTime); + } + } + } // public abstract partial class FneSystemBase : fnecore.FneSystemBase +} \ No newline at end of file diff --git a/WhackerLinkConsoleV2/FneSystemBase.cs b/WhackerLinkConsoleV2/FneSystemBase.cs new file mode 100644 index 0000000..6066580 --- /dev/null +++ b/WhackerLinkConsoleV2/FneSystemBase.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using fnecore.DMR; +using fnecore; + +namespace WhackerLinkConsoleV2 +{ + /// + /// Represents the individual timeslot data status. + /// + public class SlotStatus + { + /// + /// Rx Start Time + /// + public DateTime RxStart = DateTime.Now; + + /// + /// + /// + public uint RxSeq = 0; + + /// + /// Rx RF Source + /// + public uint RxRFS = 0; + /// + /// Tx RF Source + /// + public uint TxRFS = 0; + + /// + /// Rx Stream ID + /// + public uint RxStreamId = 0; + /// + /// Tx Stream ID + /// + public uint TxStreamId = 0; + + /// + /// Rx TG ID + /// + public uint RxTGId = 0; + /// + /// Tx TG ID + /// + public uint TxTGId = 0; + /// + /// Tx Privacy TG ID + /// + public uint TxPITGId = 0; + + /// + /// Rx Time + /// + public DateTime RxTime = DateTime.Now; + /// + /// Tx Time + /// + public DateTime TxTime = DateTime.Now; + + /// + /// Rx Type + /// + public FrameType RxType = FrameType.TERMINATOR; + + /** DMR Data */ + /// + /// Rx Link Control Header + /// + public LC DMR_RxLC = null; + /// + /// Rx Privacy Indicator Link Control Header + /// + public PrivacyLC DMR_RxPILC = null; + /// + /// Tx Link Control Header + /// + public LC DMR_TxHLC = null; + /// + /// Tx Privacy Link Control Header + /// + public PrivacyLC DMR_TxPILC = null; + /// + /// Tx Terminator Link Control + /// + public LC DMR_TxTLC = null; + } // public class SlotStatus + + /// + /// Implements a FNE system. + /// + public abstract partial class FneSystemBase : fnecore.FneSystemBase + { + public List processedChunks = new List(); + + private Random rand; + + internal MainWindow mainWindow; + + // List of active calls + private List<(uint, byte)> activeTalkgroups = new List<(uint, byte)>(); + + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// + /// Instance of or + public FneSystemBase(FnePeer fne, MainWindow mainWindow) : base(fne, 0) + { + this.fne = fne; + this.mainWindow = mainWindow; + + this.rand = new Random(Guid.NewGuid().GetHashCode()); + + // hook logger callback + this.fne.Logger = (LogLevel level, string message) => + { + switch (level) + { + case LogLevel.WARNING: + Console.WriteLine(message); + break; + case LogLevel.ERROR: + Console.WriteLine(message); + break; + case LogLevel.DEBUG: + Console.WriteLine(message); + break; + case LogLevel.FATAL: + Console.WriteLine(message); + break; + case LogLevel.INFO: + default: + Console.WriteLine(message); + break; + } + }; + } + + /// + /// Stops the main execution loop for this . + /// + public override void Stop() + { + base.Stop(); + } + + /// + /// Callback used to process whether or not a peer is being ignored for traffic. + /// + /// Peer ID + /// Source Address + /// Destination Address + /// Slot Number + /// Call Type (Group or Private) + /// Frame Type + /// DMR Data Type + /// Stream ID + /// True, if peer is ignored, otherwise false. + protected override bool PeerIgnored(uint peerId, uint srcId, uint dstId, byte slot, fnecore.CallType callType, FrameType frameType, DMRDataType dataType, uint streamId) + { + return false; + } + + /// + /// Event handler used to handle a peer connected event. + /// + /// + /// + protected override void PeerConnected(object sender, PeerConnectedEvent e) + { + return; + } + + /// + /// Returns a new stream ID + /// + /// + public uint NewStreamId() + { + return (uint)rand.Next(int.MinValue, int.MaxValue); + } + + } // public abstract partial class FneSystemBase : fnecore.FneSystemBase +} \ No newline at end of file diff --git a/WhackerLinkConsoleV2/FneSystemManager.cs b/WhackerLinkConsoleV2/FneSystemManager.cs new file mode 100644 index 0000000..f0f7e9d --- /dev/null +++ b/WhackerLinkConsoleV2/FneSystemManager.cs @@ -0,0 +1,103 @@ +/* +* WhackerLink - WhackerLinkLib +* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program. If not, see . +* +* Copyright (C) 2024-2025 Caleb, K4PHP +* +*/ + +using WhackerLinkLib.Models.Radio; +using WhackerLinkLib.Network; + +namespace WhackerLinkConsoleV2 +{ + /// + /// WhackerLink peer/client websocket manager for having multiple systems + /// + public class FneSystemManager + { + private readonly Dictionary _webSocketHandlers; + + /// + /// Creates an instance of + /// + public FneSystemManager() + { + _webSocketHandlers = new Dictionary(); + } + + /// + /// Create a new for a new system + /// + /// + public void AddFneSystem(string systemId, Codeplug.System system, MainWindow mainWindow) + { + if (!_webSocketHandlers.ContainsKey(systemId)) + { + _webSocketHandlers[systemId] = new PeerSystem(mainWindow, system); + } + } + + /// + /// Return a by looking up a systemid + /// + /// + /// + /// + public PeerSystem GetFneSystem(string systemId) + { + if (_webSocketHandlers.TryGetValue(systemId, out var handler)) + { + return handler; + } + throw new KeyNotFoundException($"WebSocketHandler for system '{systemId}' not found."); + } + + /// + /// Delete a by system id + /// + /// + public void RemoveFneSystem(string systemId) + { + if (_webSocketHandlers.TryGetValue(systemId, out var handler)) + { + handler.peer.Stop(); + _webSocketHandlers.Remove(systemId); + } + } + + /// + /// Check if the manager has a handler + /// + /// + /// + public bool HasFneSystem(string systemId) + { + return _webSocketHandlers.ContainsKey(systemId); + } + + /// + /// Cleanup + /// + public void ClearAll() + { + foreach (var handler in _webSocketHandlers.Values) + { + handler.peer.Stop(); + } + _webSocketHandlers.Clear(); + } + } +} diff --git a/WhackerLinkConsoleV2/MainWindow.xaml.cs b/WhackerLinkConsoleV2/MainWindow.xaml.cs index 06c266c..bcedfec 100644 --- a/WhackerLinkConsoleV2/MainWindow.xaml.cs +++ b/WhackerLinkConsoleV2/MainWindow.xaml.cs @@ -38,6 +38,14 @@ using System.Net; using NAudio.Wave; using WhackerLinkLib.Interfaces; using WhackerLinkLib.Models.IOSP; +using fnecore.P25; +using fnecore; +using Microsoft.VisualBasic; +using System.Text; +using Nancy; +using Constants = fnecore.Constants; +using System.Security.Cryptography; +using fnecore.P25.LC.TSBK; namespace WhackerLinkConsoleV2 { @@ -71,6 +79,13 @@ namespace WhackerLinkConsoleV2 private static System.Timers.Timer _channelHoldTimer; + private Dictionary systemStatuses = new Dictionary(); + private FneSystemManager _fneSystemManager = new FneSystemManager(); + + private bool cryptodev = false; + + private static HashSet usedRids = new HashSet(); + public MainWindow() { #if DEBUG @@ -180,26 +195,68 @@ namespace WhackerLinkConsoleV2 offsetY += 106; } - _webSocketManager.AddWebSocketHandler(system.Name); + if (!system.IsDvm) + { + _webSocketManager.AddWebSocketHandler(system.Name); - IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); - handler.OnVoiceChannelResponse += HandleVoiceResponse; - handler.OnVoiceChannelRelease += HandleVoiceRelease; - handler.OnEmergencyAlarmResponse += HandleEmergencyAlarmResponse; - handler.OnAudioData += HandleReceivedAudio; - handler.OnAffiliationUpdate += HandleAffiliationUpdate; + IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); + handler.OnVoiceChannelResponse += HandleVoiceResponse; + handler.OnVoiceChannelRelease += HandleVoiceRelease; + handler.OnEmergencyAlarmResponse += HandleEmergencyAlarmResponse; + handler.OnAudioData += HandleReceivedAudio; + handler.OnAffiliationUpdate += HandleAffiliationUpdate; - if (!_settingsManager.ShowSystemStatus) - systemStatusBox.Visibility = Visibility.Collapsed; + handler.OnUnitRegistrationResponse += (response) => + { + Dispatcher.Invoke(() => + { + if (response.Status == (int)ResponseType.GRANT) + { + systemStatusBox.Background = (Brush)new BrushConverter().ConvertFrom("#FF00BC48"); + systemStatusBox.ConnectionState = "Connected"; + } + else + { + systemStatusBox.Background = new SolidColorBrush(Colors.Red); + systemStatusBox.ConnectionState = "Disconnected"; + } + }); + }; - handler.OnUnitRegistrationResponse += (response) => - { - Dispatcher.Invoke(() => + handler.OnClose += () => { - if (response.Status == (int)ResponseType.GRANT) + Dispatcher.Invoke(() => { - systemStatusBox.Background = (Brush)new BrushConverter().ConvertFrom("#FF00BC48"); - systemStatusBox.ConnectionState = "Connected"; + systemStatusBox.Background = new SolidColorBrush(Colors.Red); + systemStatusBox.ConnectionState = "Disconnected"; + }); + }; + + handler.OnOpen += () => + { + Console.WriteLine("Peer connected"); + }; + + handler.OnReconnecting += () => + { + Console.WriteLine("Peer reconnecting"); + }; + + Task.Run(() => + { + handler.Connect(system.Address, system.Port, system.AuthKey); + + handler.OnGroupAffiliationResponse += (response) => { /* TODO */ }; + + if (handler.IsConnected) + { + U_REG_REQ release = new U_REG_REQ + { + SrcId = system.Rid, + Site = system.Site + }; + + handler.SendMessage(release.GetData()); } else { @@ -207,49 +264,45 @@ namespace WhackerLinkConsoleV2 systemStatusBox.ConnectionState = "Disconnected"; } }); - }; - - handler.OnClose += () => + } else { - Dispatcher.Invoke(() => - { - systemStatusBox.Background = new SolidColorBrush(Colors.Red); - systemStatusBox.ConnectionState = "Disconnected"; - }); - }; + _fneSystemManager.AddFneSystem(system.Name, system, this); - handler.OnOpen += () => - { - Console.WriteLine("Peer connected"); - }; + systemStatuses.Add(system.Name, new SlotStatus()); - handler.OnReconnecting += () => - { - Console.WriteLine("Peer reconnecting"); - }; + PeerSystem peer = _fneSystemManager.GetFneSystem(system.Name); - Task.Factory.StartNew(() => - { - handler.Connect(system.Address, system.Port, system.AuthKey); + peer.peer.PeerConnected += (sender, response) => + { + Console.WriteLine("FNE Peer connected"); - handler.OnGroupAffiliationResponse += (response) => { /* TODO */ }; + Dispatcher.Invoke(() => + { + systemStatusBox.Background = (Brush)new BrushConverter().ConvertFrom("#FF00BC48"); + systemStatusBox.ConnectionState = "Connected"; + }); + }; - if (handler.IsConnected) + peer.peer.PeerDisconnected += (response) => { - U_REG_REQ release = new U_REG_REQ + Console.WriteLine("FNE Peer disconnected"); + + Dispatcher.Invoke(() => { - SrcId = system.Rid, - Site = system.Site - }; + systemStatusBox.Background = new SolidColorBrush(Colors.Red); + systemStatusBox.ConnectionState = "Disconnected"; + }); + }; - handler.SendMessage(release.GetData()); - } - else + Task.Run(() => { - systemStatusBox.Background = new SolidColorBrush(Colors.Red); - systemStatusBox.ConnectionState = "Disconnected"; - } - }); + peer.Start(); + }); + } + + if (!_settingsManager.ShowSystemStatus) + systemStatusBox.Visibility = Visibility.Collapsed; + } } @@ -372,31 +425,64 @@ namespace WhackerLinkConsoleV2 Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); - IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); - if (channel.IsSelected && channel.VoiceChannel != null && channel.PttState) + Task.Run(() => { - isAnyTgOn = true; - object voicePaket = new + if (!system.IsDvm) { - type = PacketType.AUDIO_DATA, - data = new + IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); + + if (channel.IsSelected && channel.VoiceChannel != null && channel.PttState) { - Data = e.Buffer, - VoiceChannel = new VoiceChannel + isAnyTgOn = true; + + object voicePaket = new { - Frequency = channel.VoiceChannel, - DstId = cpgChannel.Tgid, - SrcId = system.Rid, - Site = system.Site - }, - Site = system.Site + type = PacketType.AUDIO_DATA, + data = new + { + Data = e.Buffer, + VoiceChannel = new VoiceChannel + { + Frequency = channel.VoiceChannel, + DstId = cpgChannel.Tgid, + SrcId = system.Rid, + Site = system.Site + }, + Site = system.Site + } + }; + + handler.SendMessage(voicePaket); } - }; + } + else + { + PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name); - handler.SendMessage(voicePaket); - } + if (channel.IsSelected && channel.PttState) + { + isAnyTgOn = true; + + int samples = 320; + + channel.chunkedPcm = AudioConverter.SplitToChunks(e.Buffer); + + foreach (byte[] chunk in channel.chunkedPcm) + { + if (chunk.Length == samples) + { + P25EncodeAudioFrame(chunk, handler, channel, cpgChannel, system); + } + else + { + Console.WriteLine("bad sample length: " + chunk.Length); + } + } + } + } + }); } if (isAnyTgOn && playbackChannelBox.IsSelected) @@ -412,21 +498,38 @@ namespace WhackerLinkConsoleV2 Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); - IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); + + if (!system.IsDvm) { + IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); - if (channel.IsSelected && handler.IsConnected) - { - Task.Run(() => + if (channel.IsSelected && handler.IsConnected) { - GRP_AFF_REQ release = new GRP_AFF_REQ + Console.WriteLine("sending WLINK master aff"); + + Task.Run(() => { - SrcId = system.Rid, - DstId = cpgChannel.Tgid, - Site = system.Site - }; + GRP_AFF_REQ release = new GRP_AFF_REQ + { + SrcId = system.Rid, + DstId = cpgChannel.Tgid, + Site = system.Site + }; - handler.SendMessage(release.GetData()); - }); + handler.SendMessage(release.GetData()); + }); + } + } else + { + PeerSystem fne = _fneSystemManager.GetFneSystem(system.Name); + + if (channel.IsSelected) + { + uint uniqueRid = GetUniqueRid(system.Rid); + + Console.WriteLine("sending FNE master aff " + uniqueRid); + + fne.peer.SendMasterGroupAffiliation(uniqueRid, UInt32.Parse(cpgChannel.Tgid)); + } } } } @@ -445,15 +548,39 @@ namespace WhackerLinkConsoleV2 pageWindow.Owner = this; if (pageWindow.ShowDialog() == true) { - IPeer handler = _webSocketManager.GetWebSocketHandler(pageWindow.RadioSystem.Name); + if (!pageWindow.RadioSystem.IsDvm) + { + IPeer handler = _webSocketManager.GetWebSocketHandler(pageWindow.RadioSystem.Name); - CALL_ALRT_REQ callAlert = new CALL_ALRT_REQ + CALL_ALRT_REQ callAlert = new CALL_ALRT_REQ + { + SrcId = pageWindow.RadioSystem.Rid, + DstId = pageWindow.DstId + }; + + handler.SendMessage(callAlert.GetData()); + } + else { - SrcId = pageWindow.RadioSystem.Rid, - DstId = pageWindow.DstId - }; + PeerSystem handler = _fneSystemManager.GetFneSystem(pageWindow.RadioSystem.Name); + IOSP_CALL_ALRT callAlert = new IOSP_CALL_ALRT(UInt32.Parse(pageWindow.DstId), UInt32.Parse(pageWindow.RadioSystem.Rid)); + + RemoteCallData callData = new RemoteCallData + { + SrcId = UInt32.Parse(pageWindow.RadioSystem.Rid), + DstId = UInt32.Parse(pageWindow.DstId), + LCO = P25Defines.TSBK_IOSP_CALL_ALRT + }; + + byte[] tsbk = new byte[P25Defines.P25_TSBK_LENGTH_BYTES]; + byte[] payload = new byte[P25Defines.P25_TSBK_LENGTH_BYTES]; + + callAlert.Encode(ref tsbk, ref payload, true, true); + + handler.SendP25TSBK(callData, tsbk); - handler.SendMessage(callAlert.GetData()); + Console.WriteLine("sent page"); + } } } @@ -467,7 +594,6 @@ namespace WhackerLinkConsoleV2 { Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); - IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); if (channel.PageState) { @@ -486,7 +612,7 @@ namespace WhackerLinkConsoleV2 int chunkSize = 1600; int totalChunks = (combinedAudio.Length + chunkSize - 1) / chunkSize; - Task.Factory.StartNew(() => + Task.Run(() => { //_waveProvider.ClearBuffer(); _audioManager.AddTalkgroupStream(cpgChannel.Tgid, combinedAudio); @@ -502,36 +628,57 @@ namespace WhackerLinkConsoleV2 byte[] chunk = new byte[chunkSize]; Buffer.BlockCopy(combinedAudio, offset, chunk, 0, size); - AudioPacket voicePacket = new AudioPacket + if (!system.IsDvm) { - Data = chunk, - VoiceChannel = new VoiceChannel + IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); + + AudioPacket voicePacket = new AudioPacket { - Frequency = channel.VoiceChannel, - DstId = cpgChannel.Tgid, - SrcId = system.Rid, - Site = system.Site - }, - Site = system.Site, - LopServerVocode = true - }; + Data = chunk, + VoiceChannel = new VoiceChannel + { + Frequency = channel.VoiceChannel, + DstId = cpgChannel.Tgid, + SrcId = system.Rid, + Site = system.Site + }, + Site = system.Site, + LopServerVocode = true + }; - handler.SendMessage(voicePacket.GetData()); + handler.SendMessage(voicePacket.GetData()); + } else + { + PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name); + // send to DVM + } } }); - double totalDurationMs = (toneADuration + toneBDuration) * 1000 + 250; + double totalDurationMs = (toneADuration + toneBDuration) + 250; await Task.Delay((int)totalDurationMs); - GRP_VCH_RLS release = new GRP_VCH_RLS + if (!system.IsDvm) { - SrcId = system.Rid, - DstId = cpgChannel.Tgid, - Channel = channel.VoiceChannel, - Site = system.Site - }; + IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); - handler.SendMessage(release.GetData()); + GRP_VCH_RLS release = new GRP_VCH_RLS + { + SrcId = system.Rid, + DstId = cpgChannel.Tgid, + Channel = channel.VoiceChannel, + Site = system.Site + }; + + handler.SendMessage(release.GetData()); + } else + { + PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name); + + channel.txStreamId = handler.NewStreamId(); + + handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), false); + } Dispatcher.Invoke(() => { @@ -545,7 +692,7 @@ namespace WhackerLinkConsoleV2 private void SendAlertTone(AlertTone e) { - Task.Factory.StartNew(() => SendAlertTone(e.AlertFilePath)); + Task.Run(() => SendAlertTone(e.AlertFilePath)); } private void SendAlertTone(string filePath, bool forHold = false) @@ -561,13 +708,12 @@ namespace WhackerLinkConsoleV2 Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); - IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); if (channel.PageState || (forHold && channel.HoldState)) { byte[] pcmData; - Task.Factory.StartNew(async () => { + Task.Run(async () => { using (var waveReader = new WaveFileReader(filePath)) { if (waveReader.WaveFormat.Encoding != WaveFormatEncoding.Pcm || @@ -596,7 +742,7 @@ namespace WhackerLinkConsoleV2 pcmData = paddedData; } - Task.Factory.StartNew(() => + Task.Run(() => { _audioManager.AddTalkgroupStream(cpgChannel.Tgid, pcmData); }); @@ -609,21 +755,40 @@ namespace WhackerLinkConsoleV2 byte[] chunk = new byte[chunkSize]; Buffer.BlockCopy(pcmData, offset, chunk, 0, chunkSize); - AudioPacket voicePacket = new AudioPacket + if (!system.IsDvm) { - Data = chunk, - VoiceChannel = new VoiceChannel + IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); + + AudioPacket voicePacket = new AudioPacket { - Frequency = channel.VoiceChannel, - DstId = cpgChannel.Tgid, - SrcId = system.Rid, - Site = system.Site - }, - Site = system.Site, - LopServerVocode = true - }; + Data = chunk, + VoiceChannel = new VoiceChannel + { + Frequency = channel.VoiceChannel, + DstId = cpgChannel.Tgid, + SrcId = system.Rid, + Site = system.Site + }, + Site = system.Site, + LopServerVocode = true + }; + + handler.SendMessage(voicePacket.GetData()); + } + else + { + PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name); - handler.SendMessage(voicePacket.GetData()); + channel.chunkedPcm = AudioConverter.SplitToChunks(chunk); + + foreach (byte[] smallchunk in channel.chunkedPcm) + { + if (smallchunk.Length == 320) + { + P25EncodeAudioFrame(smallchunk, handler, channel, cpgChannel, system); + } + } + } DateTime nextPacketTime = startTime.AddMilliseconds((i + 1) * 100); TimeSpan waitTime = nextPacketTime - DateTime.UtcNow; @@ -634,27 +799,43 @@ namespace WhackerLinkConsoleV2 } } - double totalDurationMs = ((double)pcmData.Length / 16000); + double totalDurationMs = ((double)pcmData.Length / 16000) + 250; await Task.Delay((int)totalDurationMs); - GRP_VCH_RLS release = new GRP_VCH_RLS + if (!system.IsDvm) { - SrcId = system.Rid, - DstId = cpgChannel.Tgid, - Channel = channel.VoiceChannel, - Site = system.Site - }; + IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); - Dispatcher.Invoke(() => - { - handler.SendMessage(release.GetData()); + GRP_VCH_RLS release = new GRP_VCH_RLS + { + SrcId = system.Rid, + DstId = cpgChannel.Tgid, + Channel = channel.VoiceChannel, + Site = system.Site + }; - if (forHold) - channel.PttButton.Background = channel.grayGradient; - else - channel.PageState = false; - }); + Dispatcher.Invoke(() => + { + handler.SendMessage(release.GetData()); + + if (forHold) + channel.PttButton.Background = channel.grayGradient; + else + channel.PageState = false; + }); + } else + { + PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name); + handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), false); + Dispatcher.Invoke(() => + { + if (forHold) + channel.PttButton.Background = channel.grayGradient; + else + channel.PageState = false; + }); + } }); } } @@ -724,7 +905,9 @@ namespace WhackerLinkConsoleV2 Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); - IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); + + if (system.IsDvm) + continue; if (audioPacket.VoiceChannel.SrcId != system.Rid && audioPacket.VoiceChannel.Frequency == channel.VoiceChannel && audioPacket.VoiceChannel.DstId == cpgChannel.Tgid) shouldReceive = true; @@ -743,6 +926,10 @@ namespace WhackerLinkConsoleV2 Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); + + if (system.IsDvm) + continue; + IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); bool ridExists = affUpdate.Affiliations.Any(aff => aff.SrcId == system.Rid); @@ -779,6 +966,10 @@ namespace WhackerLinkConsoleV2 Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); + + if (system.IsDvm) + continue; + IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); if (response.DstId == cpgChannel.Tgid && response.SrcId != system.Rid) @@ -811,6 +1002,10 @@ namespace WhackerLinkConsoleV2 Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); + + if (system.IsDvm) + continue; + IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); if (channel.PttState && response.Status == (int)ResponseType.GRANT && response.Channel != null && response.SrcId == system.Rid && response.DstId == cpgChannel.Tgid) @@ -854,6 +1049,10 @@ namespace WhackerLinkConsoleV2 Codeplug.System system = Codeplug.GetSystemForChannel(e.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(e.ChannelName); + + if (system.IsDvm) + return; + IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); } @@ -864,33 +1063,50 @@ namespace WhackerLinkConsoleV2 Codeplug.System system = Codeplug.GetSystemForChannel(e.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(e.ChannelName); - IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); - - if (e.PageState) - { - GRP_VCH_REQ request = new GRP_VCH_REQ - { - SrcId = system.Rid, - DstId = cpgChannel.Tgid, - Site = system.Site - }; - handler.SendMessage(request.GetData()); - } - else + if (!system.IsDvm) { - GRP_VCH_RLS release = new GRP_VCH_RLS - { - SrcId = system.Rid, - DstId = cpgChannel.Tgid, - Channel = e.VoiceChannel, - Site = system.Site - }; - handler.SendMessage(release.GetData()); - e.VoiceChannel = null; - } - } + IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); + + if (e.PageState) + { + GRP_VCH_REQ request = new GRP_VCH_REQ + { + SrcId = system.Rid, + DstId = cpgChannel.Tgid, + Site = system.Site + }; + + handler.SendMessage(request.GetData()); + } + else + { + GRP_VCH_RLS release = new GRP_VCH_RLS + { + SrcId = system.Rid, + DstId = cpgChannel.Tgid, + Channel = e.VoiceChannel, + Site = system.Site + }; + + handler.SendMessage(release.GetData()); + e.VoiceChannel = null; + } + } + else + { + PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name); + if (e.PageState) + { + handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), true); + } + else + { + handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), false); + } + } + } private void ChannelBox_PTTButtonClicked(object sender, ChannelBox e) { @@ -899,34 +1115,60 @@ namespace WhackerLinkConsoleV2 Codeplug.System system = Codeplug.GetSystemForChannel(e.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(e.ChannelName); - IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); - if (!e.IsSelected) - return; - - if (e.PttState) + if (!system.IsDvm) { - GRP_VCH_REQ request = new GRP_VCH_REQ + IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); + + if (!e.IsSelected) + return; + + if (e.PttState) { - SrcId = system.Rid, - DstId = cpgChannel.Tgid, - Site = system.Site - }; + GRP_VCH_REQ request = new GRP_VCH_REQ + { + SrcId = system.Rid, + DstId = cpgChannel.Tgid, + Site = system.Site + }; - handler.SendMessage(request.GetData()); - } - else + handler.SendMessage(request.GetData()); + } + else + { + GRP_VCH_RLS release = new GRP_VCH_RLS + { + SrcId = system.Rid, + DstId = cpgChannel.Tgid, + Channel = e.VoiceChannel, + Site = system.Site + }; + + handler.SendMessage(release.GetData()); + e.VoiceChannel = null; + } + } else { - GRP_VCH_RLS release = new GRP_VCH_RLS + PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name); + + if (!e.IsSelected) + return; + + uint srcId = UInt32.Parse(system.Rid); + uint dstId = UInt32.Parse(cpgChannel.Tgid); + + if (e.PttState) { - SrcId = system.Rid, - DstId = cpgChannel.Tgid, - Channel = e.VoiceChannel, - Site = system.Site - }; + e.txStreamId = handler.NewStreamId(); - handler.SendMessage(release.GetData()); - e.VoiceChannel = null; + Console.WriteLine("sending grant demand " + dstId); + handler.SendP25TDU(srcId, dstId, true); + } + else + { + Console.WriteLine("sending terminator " + dstId); + handler.SendP25TDU(srcId, dstId, false); + } } } @@ -1113,29 +1355,45 @@ namespace WhackerLinkConsoleV2 Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); - IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); - if (channel.HoldState && !channel.IsReceiving && !channel.PttState && !channel.PageState) + if (!system.IsDvm) { - //Task.Factory.StartNew(async () => - //{ - Console.WriteLine("Sending channel hold beep"); - Dispatcher.Invoke(() => { channel.PttButton.Background = channel.redGradient; }); + IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); - GRP_VCH_REQ req = new GRP_VCH_REQ + if (channel.HoldState && !channel.IsReceiving && !channel.PttState && !channel.PageState) { - SrcId = system.Rid, - DstId = cpgChannel.Tgid, - Site = system.Site - }; + //Task.Factory.StartNew(async () => + //{ + Console.WriteLine("Sending channel hold beep"); + + Dispatcher.Invoke(() => { channel.PttButton.Background = channel.redGradient; }); + + GRP_VCH_REQ req = new GRP_VCH_REQ + { + SrcId = system.Rid, + DstId = cpgChannel.Tgid, + Site = system.Site + }; - handler.SendMessage(req.GetData()); + handler.SendMessage(req.GetData()); - await Task.Delay(1000); + await Task.Delay(1000); - SendAlertTone("hold.wav", true); - // }); + SendAlertTone("hold.wav", true); + // }); + } + } else + { + PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name); + + if (channel.HoldState && !channel.IsReceiving && !channel.PttState && !channel.PageState) + { + handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), true); + await Task.Delay(1000); + + SendAlertTone("hold.wav", true); + } } } } @@ -1180,8 +1438,11 @@ namespace WhackerLinkConsoleV2 }); } - private void btnGlobalPtt_Click(object sender, RoutedEventArgs e) + private async void btnGlobalPtt_Click(object sender, RoutedEventArgs e) { + if (globalPttState) + await Task.Delay(500); + globalPttState = !globalPttState; foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels()) @@ -1191,51 +1452,606 @@ namespace WhackerLinkConsoleV2 Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); - IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); - - if (!channel.IsSelected) - return; - if (globalPttState) + if (!system.IsDvm) { - Dispatcher.Invoke(() => + IPeer handler = _webSocketManager.GetWebSocketHandler(system.Name); + + if (!channel.IsSelected) + continue; + + if (globalPttState) { - btnGlobalPtt.Background = channel.redGradient; - }); + Dispatcher.Invoke(() => + { + btnGlobalPtt.Background = channel.redGradient; + }); - GRP_VCH_REQ request = new GRP_VCH_REQ + GRP_VCH_REQ request = new GRP_VCH_REQ + { + SrcId = system.Rid, + DstId = cpgChannel.Tgid, + Site = system.Site + }; + + channel.PttState = true; + + handler.SendMessage(request.GetData()); + } + else { - SrcId = system.Rid, - DstId = cpgChannel.Tgid, - Site = system.Site - }; + Dispatcher.Invoke(() => + { + btnGlobalPtt.Background = channel.grayGradient; + }); - channel.PttState = true; + GRP_VCH_RLS release = new GRP_VCH_RLS + { + SrcId = system.Rid, + DstId = cpgChannel.Tgid, + Site = system.Site + }; - handler.SendMessage(request.GetData()); + channel.PttState = false; + + handler.SendMessage(release.GetData()); + } + } else + { + PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name); + + channel.txStreamId = handler.NewStreamId(); + + if (globalPttState) + { + Dispatcher.Invoke(() => + { + btnGlobalPtt.Background = channel.redGradient; + channel.PttState = true; + }); + + handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), true); + } + else + { + Dispatcher.Invoke(() => + { + btnGlobalPtt.Background = channel.grayGradient; + channel.PttState = false; + }); + + handler.SendP25TDU(UInt32.Parse(system.Rid), UInt32.Parse(cpgChannel.Tgid), false); + } } + } + } + + private void Button_Click(object sender, RoutedEventArgs e) { /* sub */ } + + /// + /// Helper to encode and transmit PCM audio as P25 IMBE frames. + /// + private void P25EncodeAudioFrame(byte[] pcm, PeerSystem handler, ChannelBox channel, Codeplug.Channel cpgChannel, Codeplug.System system) + { + if (channel.p25N > 17) + channel.p25N = 0; + if (channel.p25N == 0) + FneUtils.Memset(channel.netLDU1, 0, 9 * 25); + if (channel.p25N == 9) + FneUtils.Memset(channel.netLDU2, 0, 9 * 25); + + // Log.Logger.Debug($"BYTE BUFFER {FneUtils.HexDump(pcm)}"); + + //// pre-process: apply gain to PCM audio frames + //if (Program.Configuration.TxAudioGain != 1.0f) + //{ + // BufferedWaveProvider buffer = new BufferedWaveProvider(waveFormat); + // buffer.AddSamples(pcm, 0, pcm.Length); + + // VolumeWaveProvider16 gainControl = new VolumeWaveProvider16(buffer); + // gainControl.Volume = Program.Configuration.TxAudioGain; + // gainControl.Read(pcm, 0, pcm.Length); + //} + + int smpIdx = 0; + short[] samples = new short[FneSystemBase.MBE_SAMPLES_LENGTH]; + for (int pcmIdx = 0; pcmIdx < pcm.Length; pcmIdx += 2) + { + samples[smpIdx] = (short)((pcm[pcmIdx + 1] << 8) + pcm[pcmIdx + 0]); + smpIdx++; + } + + // Log.Logger.Debug($"SAMPLE BUFFER {FneUtils.HexDump(samples)}"); + + // encode PCM samples into IMBE codewords + byte[] imbe = new byte[FneSystemBase.IMBE_BUF_LEN]; + +#if WIN32 + if (channel.extFullRateVocoder == null) + channel.extFullRateVocoder = new AmbeVocoder(true); + + channel.extFullRateVocoder.encode(samples, out imbe); +#else + if (channel.encoder == null) + channel.encoder = new MBEEncoder(MBE_MODE.IMBE_88BIT); + + channel.encoder.encode(samples, imbe); +#endif + // Log.Logger.Debug($"IMBE {FneUtils.HexDump(imbe)}"); + + // fill the LDU buffers appropriately + switch (channel.p25N) + { + // LDU1 + case 0: + Buffer.BlockCopy(imbe, 0, channel.netLDU1, 10, FneSystemBase.IMBE_BUF_LEN); + break; + case 1: + Buffer.BlockCopy(imbe, 0, channel.netLDU1, 26, FneSystemBase.IMBE_BUF_LEN); + break; + case 2: + Buffer.BlockCopy(imbe, 0, channel.netLDU1, 55, FneSystemBase.IMBE_BUF_LEN); + break; + case 3: + Buffer.BlockCopy(imbe, 0, channel.netLDU1, 80, FneSystemBase.IMBE_BUF_LEN); + break; + case 4: + Buffer.BlockCopy(imbe, 0, channel.netLDU1, 105, FneSystemBase.IMBE_BUF_LEN); + break; + case 5: + Buffer.BlockCopy(imbe, 0, channel.netLDU1, 130, FneSystemBase.IMBE_BUF_LEN); + break; + case 6: + Buffer.BlockCopy(imbe, 0, channel.netLDU1, 155, FneSystemBase.IMBE_BUF_LEN); + break; + case 7: + Buffer.BlockCopy(imbe, 0, channel.netLDU1, 180, FneSystemBase.IMBE_BUF_LEN); + break; + case 8: + Buffer.BlockCopy(imbe, 0, channel.netLDU1, 204, FneSystemBase.IMBE_BUF_LEN); + break; + + // LDU2 + case 9: + Buffer.BlockCopy(imbe, 0, channel.netLDU2, 10, FneSystemBase.IMBE_BUF_LEN); + break; + case 10: + Buffer.BlockCopy(imbe, 0, channel.netLDU2, 26, FneSystemBase.IMBE_BUF_LEN); + break; + case 11: + Buffer.BlockCopy(imbe, 0, channel.netLDU2, 55, FneSystemBase.IMBE_BUF_LEN); + break; + case 12: + Buffer.BlockCopy(imbe, 0, channel.netLDU2, 80, FneSystemBase.IMBE_BUF_LEN); + break; + case 13: + Buffer.BlockCopy(imbe, 0, channel.netLDU2, 105, FneSystemBase.IMBE_BUF_LEN); + break; + case 14: + Buffer.BlockCopy(imbe, 0, channel.netLDU2, 130, FneSystemBase.IMBE_BUF_LEN); + break; + case 15: + Buffer.BlockCopy(imbe, 0, channel.netLDU2, 155, FneSystemBase.IMBE_BUF_LEN); + break; + case 16: + Buffer.BlockCopy(imbe, 0, channel.netLDU2, 180, FneSystemBase.IMBE_BUF_LEN); + break; + case 17: + Buffer.BlockCopy(imbe, 0, channel.netLDU2, 204, FneSystemBase.IMBE_BUF_LEN); + break; + } + + uint srcId = UInt32.Parse(system.Rid); + uint dstId = UInt32.Parse(cpgChannel.Tgid); + + FnePeer peer = handler.peer; + RemoteCallData callData = new RemoteCallData() + { + SrcId = srcId, + DstId = dstId, + LCO = P25Defines.LC_GROUP + }; + + // send P25 LDU1 + if (channel.p25N == 8U) + { + ushort pktSeq = 0; + if (channel.p25SeqNo == 0U) + pktSeq = peer.pktSeq(true); + else + pktSeq = peer.pktSeq(); + + //Console.WriteLine($"({channel.SystemName}) P25D: Traffic *VOICE FRAME * PEER {handler.PeerId} SRC_ID {srcId} TGID {dstId} [STREAM ID {channel.txStreamId}]"); + + byte[] payload = new byte[200]; + handler.CreateNewP25MessageHdr((byte)P25DUID.LDU1, callData, ref payload); + handler.CreateP25LDU1Message(channel.netLDU1, ref payload, srcId, dstId); + + peer.SendMaster(new Tuple(Constants.NET_FUNC_PROTOCOL, Constants.NET_PROTOCOL_SUBFUNC_P25), payload, pktSeq, channel.txStreamId); + } + + // send P25 LDU2 + if (channel.p25N == 17U) + { + ushort pktSeq = 0; + if (channel.p25SeqNo == 0U) + pktSeq = peer.pktSeq(true); else + pktSeq = peer.pktSeq(); + + //Console.WriteLine($"({channel.SystemName}) P25D: Traffic *VOICE FRAME * PEER {handler.PeerId} SRC_ID {srcId} TGID {dstId} [STREAM ID {channel.txStreamId}]"); + + byte[] payload = new byte[200]; + handler.CreateNewP25MessageHdr((byte)P25DUID.LDU2, callData, ref payload); + handler.CreateP25LDU2Message(channel.netLDU2, ref payload); + + peer.SendMaster(new Tuple(Constants.NET_FUNC_PROTOCOL, Constants.NET_PROTOCOL_SUBFUNC_P25), payload, pktSeq, channel.txStreamId); + } + + channel.p25SeqNo++; + channel.p25N++; + } + + /// + /// Helper to decode and playback P25 IMBE frames as PCM audio. + /// + /// + /// + private void P25DecodeAudioFrame(byte[] ldu, P25DataReceivedEvent e, PeerSystem system, ChannelBox channel, bool emergency = false, P25Crypto.FrameType frameType = P25Crypto.FrameType.LDU1) + { + try + { + // decode 9 IMBE codewords into PCM samples + for (int n = 0; n < 9; n++) { - Dispatcher.Invoke(() => + byte[] imbe = new byte[FneSystemBase.IMBE_BUF_LEN]; + switch (n) { - btnGlobalPtt.Background = channel.grayGradient; - }); + case 0: + Buffer.BlockCopy(ldu, 10, imbe, 0, FneSystemBase.IMBE_BUF_LEN); + break; + case 1: + Buffer.BlockCopy(ldu, 26, imbe, 0, FneSystemBase.IMBE_BUF_LEN); + break; + case 2: + Buffer.BlockCopy(ldu, 55, imbe, 0, FneSystemBase.IMBE_BUF_LEN); + break; + case 3: + Buffer.BlockCopy(ldu, 80, imbe, 0, FneSystemBase.IMBE_BUF_LEN); + break; + case 4: + Buffer.BlockCopy(ldu, 105, imbe, 0, FneSystemBase.IMBE_BUF_LEN); + break; + case 5: + Buffer.BlockCopy(ldu, 130, imbe, 0, FneSystemBase.IMBE_BUF_LEN); + break; + case 6: + Buffer.BlockCopy(ldu, 155, imbe, 0, FneSystemBase.IMBE_BUF_LEN); + break; + case 7: + Buffer.BlockCopy(ldu, 180, imbe, 0, FneSystemBase.IMBE_BUF_LEN); + break; + case 8: + Buffer.BlockCopy(ldu, 204, imbe, 0, FneSystemBase.IMBE_BUF_LEN); + break; + } - GRP_VCH_RLS release = new GRP_VCH_RLS + //Log.Logger.Debug($"Decoding IMBE buffer: {FneUtils.HexDump(imbe)}"); + + short[] samples = new short[FneSystemBase.MBE_SAMPLES_LENGTH]; + int errs = 0; +#if WIN32 + if (channel.extFullRateVocoder == null) + channel.extFullRateVocoder = new AmbeVocoder(true); + + errs = channel.extFullRateVocoder.decode(imbe, out samples); +#else + if (cryptodev) { - SrcId = system.Rid, - DstId = cpgChannel.Tgid, - Site = system.Site - }; + Console.WriteLine($"MI: {FneUtils.HexDump(channel.mi)}"); + Console.WriteLine($"Algorithm ID: {channel.algId}"); + Console.WriteLine($"Key ID: {channel.kId}"); - channel.PttState = false; + channel.crypter.Process(imbe, frameType, n); + } - handler.SendMessage(release.GetData()); + //Console.WriteLine(FneUtils.HexDump(imbe)); + + errs = channel.decoder.decode(imbe, samples); +#endif + + if (emergency) + { + if (!channel.Emergency) + { + Task.Run(() => + { + HandleEmergencyAlarmResponse(new EMRG_ALRM_RSP + { + SrcId = e.SrcId.ToString(), + DstId = e.DstId.ToString() + }); + }); + } + } + + if (samples != null) + { + //Log.Logger.Debug($"({Config.Name}) P25D: Traffic *VOICE FRAME * PEER {e.PeerId} SRC_ID {e.SrcId} TGID {e.DstId} VC{n} ERRS {errs} [STREAM ID {e.StreamId}]"); + //Log.Logger.Debug($"IMBE {FneUtils.HexDump(imbe)}"); + //Console.WriteLine($"SAMPLE BUFFER {FneUtils.HexDump(samples)}"); + + int pcmIdx = 0; + byte[] pcmData = new byte[samples.Length * 2]; + for (int i = 0; i < samples.Length; i++) + { + pcmData[pcmIdx] = (byte)(samples[i] & 0xFF); + pcmData[pcmIdx + 1] = (byte)((samples[i] >> 8) & 0xFF); + pcmIdx += 2; + } + + _audioManager.AddTalkgroupStream(e.DstId.ToString(), pcmData); + } } } + catch (Exception ex) + { + Console.WriteLine($"Audio Decode Exception: {ex.Message}"); + } } - private void Button_Click(object sender, RoutedEventArgs e) { /* sub */ } + private uint GetUniqueRid(string ridString) + { + uint rid; + + // Try to parse the RID, default to 1000 if parsing fails + if (!UInt32.TryParse(ridString, out rid)) + { + rid = 1000; + } + + // Ensure uniqueness by incrementing if needed + while (usedRids.Contains(rid)) + { + rid++; + } + + // Store the new unique RID + usedRids.Add(rid); + + return rid; + } + + /// + /// Event handler used to process incoming P25 data. + /// + /// + /// + public void P25DataReceived(P25DataReceivedEvent e, DateTime pktTime) + { + uint sysId = (uint)((e.Data[11U] << 8) | (e.Data[12U] << 0)); + uint netId = FneUtils.Bytes3ToUInt32(e.Data, 16); + byte control = e.Data[14U]; + + byte len = e.Data[23]; + byte[] data = new byte[len]; + for (int i = 24; i < len; i++) + data[i - 24] = e.Data[i]; + + Dispatcher.Invoke(() => + { + foreach (ChannelBox channel in _selectedChannelsManager.GetSelectedChannels()) + { + Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); + Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); + + if (!system.IsDvm) + continue; + + PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name); + + if (!channel.IsEnabled) + continue; + + if (cpgChannel.Tgid != e.DstId.ToString()) + continue; + + bool ignoreCall = false; + byte callAlgoId = 0x00; + + if (!systemStatuses.ContainsKey(system.Name)) + { + systemStatuses[system.Name] = new SlotStatus(); + } + + if (channel.decoder == null) + { + channel.decoder = new MBEDecoder(MBE_MODE.IMBE_88BIT); + } + + SlotStatus slot = systemStatuses[system.Name]; + + // is this a new call stream? + if (e.StreamId != slot.RxStreamId && ((e.DUID != P25DUID.TDU) && (e.DUID != P25DUID.TDULC))) + { + channel.IsReceiving = true; + callAlgoId = P25Defines.P25_ALGO_UNENCRYPT; + slot.RxStart = pktTime; + // Console.WriteLine($"({system.Name}) P25D: Traffic *CALL START * PEER {e.PeerId} SRC_ID {e.SrcId} TGID {e.DstId} [STREAM ID {e.StreamId}]"); + + channel.LastSrcId = "Last SRC: " + e.SrcId; + channel.Background = (Brush)new BrushConverter().ConvertFrom("#FF00BC48"); + } + + // Is the call over? + if (((e.DUID == P25DUID.TDU) || (e.DUID == P25DUID.TDULC)) && (slot.RxType != FrameType.TERMINATOR)) + { + ignoreCall = false; + callAlgoId = P25Defines.P25_ALGO_UNENCRYPT; + channel.IsReceiving = false; + TimeSpan callDuration = pktTime - slot.RxStart; + // Console.WriteLine($"({system.Name}) P25D: Traffic *CALL END * PEER {e.PeerId} SRC_ID {e.SrcId} TGID {e.DstId} DUR {callDuration} [STREAM ID {e.StreamId}]"); + channel.Background = (Brush)new BrushConverter().ConvertFrom("#FF0B004B"); + return; + } + + if (ignoreCall && callAlgoId == P25Defines.P25_ALGO_UNENCRYPT) + ignoreCall = false; + + // if this is an LDU1 see if this is the first LDU with HDU encryption data + if (e.DUID == P25DUID.LDU1 && !ignoreCall) + { + byte frameType = e.Data[180]; + if (frameType == P25Defines.P25_FT_HDU_VALID) + callAlgoId = e.Data[181]; + } + + if (e.DUID == P25DUID.LDU2 && !ignoreCall) + callAlgoId = data[88]; + + if (ignoreCall) + return; + + bool isEmergency = false; + + int count = 0; + switch (e.DUID) + { + case P25DUID.LDU1: + { + // The '62', '63', '64', '65', '66', '67', '68', '69', '6A' records are LDU1 + if ((data[0U] == 0x62U) && (data[22U] == 0x63U) && + (data[36U] == 0x64U) && (data[53U] == 0x65U) && + (data[70U] == 0x66U) && (data[87U] == 0x67U) && + (data[104U] == 0x68U) && (data[121U] == 0x69U) && + (data[138U] == 0x6AU)) + { + // The '62' record - IMBE Voice 1 + Buffer.BlockCopy(data, count, channel.netLDU1, 0, 22); + count += 22; + + // The '63' record - IMBE Voice 2 + Buffer.BlockCopy(data, count, channel.netLDU1, 25, 14); + count += 14; + + // The '64' record - IMBE Voice 3 + Link Control + Buffer.BlockCopy(data, count, channel.netLDU1, 50, 17); + + byte serviceOptions = data[count + 3]; + isEmergency = (serviceOptions & 0x80) == 0x80; + + count += 17; + + // The '65' record - IMBE Voice 4 + Link Control + Buffer.BlockCopy(data, count, channel.netLDU1, 75, 17); + count += 17; + + // The '66' record - IMBE Voice 5 + Link Control + Buffer.BlockCopy(data, count, channel.netLDU1, 100, 17); + count += 17; + + // The '67' record - IMBE Voice 6 + Link Control + Buffer.BlockCopy(data, count, channel.netLDU1, 125, 17); + count += 17; + + // The '68' record - IMBE Voice 7 + Link Control + Buffer.BlockCopy(data, count, channel.netLDU1, 150, 17); + count += 17; + + // The '69' record - IMBE Voice 8 + Link Control + Buffer.BlockCopy(data, count, channel.netLDU1, 175, 17); + count += 17; + + // The '6A' record - IMBE Voice 9 + Low Speed Data + Buffer.BlockCopy(data, count, channel.netLDU1, 200, 16); + count += 16; + + if (cryptodev) + { + if (channel.mi != null && channel.mi.Length > 0) + channel.crypter.Prepare(channel.algId, channel.kId, P25Crypto.ProtocolType.P25Phase1, channel.mi); + } + + // decode 9 IMBE codewords into PCM samples + P25DecodeAudioFrame(channel.netLDU1, e, handler, channel, isEmergency); + } + } + break; + case P25DUID.LDU2: + { + // The '6B', '6C', '6D', '6E', '6F', '70', '71', '72', '73' records are LDU2 + if ((data[0U] == 0x6BU) && (data[22U] == 0x6CU) && + (data[36U] == 0x6DU) && (data[53U] == 0x6EU) && + (data[70U] == 0x6FU) && (data[87U] == 0x70U) && + (data[104U] == 0x71U) && (data[121U] == 0x72U) && + (data[138U] == 0x73U)) + { + // The '6B' record - IMBE Voice 10 + Buffer.BlockCopy(data, count, channel.netLDU2, 0, 22); + count += 22; + + // The '6C' record - IMBE Voice 11 + Buffer.BlockCopy(data, count, channel.netLDU2, 25, 14); + count += 14; + + // The '6D' record - IMBE Voice 12 + Encryption Sync + Buffer.BlockCopy(data, count, channel.netLDU2, 50, 17); + channel.mi[0] = data[count + 1]; + channel.mi[1] = data[count + 2]; + channel.mi[2] = data[count + 3]; + count += 17; + + // The '6E' record - IMBE Voice 13 + Encryption Sync + Buffer.BlockCopy(data, count, channel.netLDU2, 75, 17); + channel.mi[3] = data[count + 1]; + channel.mi[4] = data[count + 2]; + channel.mi[5] = data[count + 3]; + count += 17; + + // The '6F' record - IMBE Voice 14 + Encryption Sync + Buffer.BlockCopy(data, count, channel.netLDU2, 100, 17); + channel.mi[6] = data[count + 1]; + channel.mi[7] = data[count + 2]; + channel.mi[8] = data[count + 3]; + count += 17; + + // The '70' record - IMBE Voice 15 + Encryption Sync + Buffer.BlockCopy(data, count, channel.netLDU2, 125, 17); + channel.algId = data[count + 1]; // Algorithm ID + channel.kId = (ushort)((data[count + 2] << 8) | data[count + 3]); // Key ID + count += 17; + + // The '71' record - IMBE Voice 16 + Encryption Sync + Buffer.BlockCopy(data, count, channel.netLDU2, 150, 17); + count += 17; + + // The '72' record - IMBE Voice 17 + Encryption Sync + Buffer.BlockCopy(data, count, channel.netLDU2, 175, 17); + count += 17; + + // The '73' record - IMBE Voice 18 + Low Speed Data + Buffer.BlockCopy(data, count, channel.netLDU2, 200, 16); + count += 16; + + if (channel.mi != null && channel.mi.Length > 0 && cryptodev) + channel.crypter.Prepare(channel.algId, channel.kId, P25Crypto.ProtocolType.P25Phase1, channel.mi); + + // decode 9 IMBE codewords into PCM samples + P25DecodeAudioFrame(channel.netLDU2, e, handler, channel, isEmergency, P25Crypto.FrameType.LDU2); + } + } + break; + } + + slot.RxRFS = e.SrcId; + slot.RxType = e.FrameType; + slot.RxTGId = e.DstId; + slot.RxTime = pktTime; + slot.RxStreamId = e.StreamId; + + } + }); + } } } diff --git a/WhackerLinkConsoleV2/P25Crypto.cs b/WhackerLinkConsoleV2/P25Crypto.cs new file mode 100644 index 0000000..cc8b01d --- /dev/null +++ b/WhackerLinkConsoleV2/P25Crypto.cs @@ -0,0 +1,205 @@ +// Based on OP25 p25_crypt_algs.cpp + +namespace WhackerLinkConsoleV2 +{ + public class P25Crypto + { + private int debug; + private int msgqId; + private ProtocolType prType; + private byte algId; + private ushort keyId; + private byte[] messageIndicator = new byte[9]; + private Dictionary keys = new Dictionary(); + private byte[] adpKeystream = new byte[469]; + private int adpPosition; + + public P25Crypto(int debug = 0, int msgqId = 0) + { + this.debug = debug; + this.msgqId = msgqId; + this.prType = ProtocolType.Unknown; + this.algId = 0x80; + this.keyId = 0; + this.adpPosition = 0; + } + + public void Reset() + { + keys.Clear(); + } + + public void AddKey(ushort keyid, byte algid, byte[] key) + { + if (keyid == 0 || algid == 0x80) + return; + + keys[keyid] = new KeyInfo(algid, key); + } + + public bool Prepare(byte algid, ushort keyid, ProtocolType prType, byte[] MI) + { + this.algId = algid; + this.keyId = keyid; + Array.Copy(MI, this.messageIndicator, Math.Min(MI.Length, this.messageIndicator.Length)); + + if (!keys.ContainsKey(keyid)) + { + if (debug >= 10) + Console.Error.WriteLine($"P25Crypto::Prepare: KeyID [0x{keyid:X}] not found"); + + return false; + } + + if (debug >= 10) + Console.WriteLine($"P25Crypto::Prepare: KeyID [0x{keyid:X}] found"); + + if (algid == 0xAA) // ADP RC4 + { + this.adpPosition = 0; + this.prType = prType; + AdpKeystreamGen(); + return true; + } + + return false; + } + + public bool Process(byte[] PCW, FrameType frameType, int voiceSubframe) + { + if (!keys.ContainsKey(keyId)) + return false; + + if (algId == 0xAA) // ADP RC4 + return AdpProcess(PCW, frameType, voiceSubframe); + + return false; + } + + private bool AdpProcess(byte[] PCW, FrameType frameType, int voiceSubframe) + { + int offset = 256; + + switch (frameType) + { + case FrameType.LDU1: offset = 0; break; + case FrameType.LDU2: offset = 101; break; + case FrameType.V4_0: offset += 7 * voiceSubframe; break; + case FrameType.V4_1: offset += 7 * (voiceSubframe + 4); break; + case FrameType.V4_2: offset += 7 * (voiceSubframe + 8); break; + case FrameType.V4_3: offset += 7 * (voiceSubframe + 12); break; + case FrameType.V2: offset += 7 * (voiceSubframe + 16); break; + default: return false; + } + + if (prType == ProtocolType.P25Phase1) + { + // FDMA + offset += (adpPosition * 11) + 267 + (adpPosition < 8 ? 0 : 2); + adpPosition = (adpPosition + 1) % 9; + for (int j = 0; j < 11; ++j) + { + PCW[j] ^= adpKeystream[j + offset]; + } + } + else if (prType == ProtocolType.P25Phase2) + { + // TDMA + for (int j = 0; j < 7; ++j) + { + PCW[j] ^= adpKeystream[j + offset]; + } + PCW[6] &= 0x80; // Mask everything except MSB of the final codeword + } + + return true; + } + + private void AdpKeystreamGen() + { + byte[] adpKey = new byte[13]; + byte[] S = new byte[256]; + byte[] K = new byte[256]; + + if (!keys.ContainsKey(keyId)) + return; + + byte[] keyData = keys[keyId].Key; + + int keySize = keyData.Length; + int padding = Math.Max(5 - keySize, 0); + int i, j = 0, k; + + for (i = 0; i < padding; i++) + adpKey[i] = 0; + + for (; i < 5; i++) + adpKey[i] = keySize > 0 ? keyData[i - padding] : (byte)0; + + // Append MI bytes + for (i = 5; i < 13; ++i) + { + adpKey[i] = messageIndicator[i - 5]; + } + + for (i = 0; i < 256; ++i) + { + K[i] = adpKey[i % 13]; + S[i] = (byte)i; + } + + for (i = 0; i < 256; ++i) + { + j = (j + S[i] + K[i]) & 0xFF; + Swap(ref S[i], ref S[j]); + } + + i = j = 0; + for (k = 0; k < 469; ++k) + { + i = (i + 1) & 0xFF; + j = (j + S[i]) & 0xFF; + Swap(ref S[i], ref S[j]); + adpKeystream[k] = S[(S[i] + S[j]) & 0xFF]; + } + } + + private void Swap(ref byte a, ref byte b) + { + byte temp = a; + a = b; + b = temp; + } + + public enum ProtocolType + { + Unknown = 0, + P25Phase1, + P25Phase2 + } + + public enum FrameType + { + Unknown = 0, + LDU1, + LDU2, + V2, + V4_0, + V4_1, + V4_2, + V4_3 + } + + private class KeyInfo + { + public byte AlgId { get; } + public byte[] Key { get; } + + public KeyInfo(byte algid, byte[] key) + { + AlgId = algid; + Key = key; + } + } + } +} diff --git a/WhackerLinkConsoleV2/PeerSystem.cs b/WhackerLinkConsoleV2/PeerSystem.cs new file mode 100644 index 0000000..a732c62 --- /dev/null +++ b/WhackerLinkConsoleV2/PeerSystem.cs @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: AGPL-3.0-only +/** +* Digital Voice Modem - Audio Bridge +* AGPLv3 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Audio Bridge +* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) +* +* Copyright (C) 2023 Bryan Biedenkapp, N2PLL +* Copyright (C) 2024 Caleb, KO4UYJ +* +*/ +using System; +using System.Net; +using System.Collections.Generic; +using System.Text; +using System.Net.Sockets; +using System.Threading.Tasks; + +using Serilog; + +using fnecore; +using WhackerLinkLib.Models.Radio; +using static WhackerLinkLib.Models.Radio.Codeplug; + +namespace WhackerLinkConsoleV2 +{ + /// + /// Implements a peer FNE router system. + /// + public class PeerSystem : FneSystemBase + { + public FnePeer peer; + + /* + ** Methods + */ + + /// + /// Initializes a new instance of the class. + /// + public PeerSystem(MainWindow mainWindow, Codeplug.System system) : base(Create(system), mainWindow) + { + peer = (FnePeer)fne; + } + + /// + /// Internal helper to instantiate a new instance of class. + /// + /// + private static FnePeer Create(Codeplug.System system) + { + IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, system.Port); + string presharedKey = null; + + if (system.Address == null) + throw new NullReferenceException("address"); + if (system.Address == string.Empty) + throw new ArgumentException("address"); + + // handle using address as IP or resolving from hostname to IP + try + { + endpoint = new IPEndPoint(IPAddress.Parse(system.Address), system.Port); + } + catch (FormatException) + { + IPAddress[] addresses = Dns.GetHostAddresses("fne.zone1.scan.stream"); + if (addresses.Length > 0) + endpoint = new IPEndPoint(addresses[0], system.Port); + } + + FnePeer peer = new FnePeer("WLINKCONSOLE", system.PeerId, endpoint, presharedKey); + + // set configuration parameters + peer.Passphrase = system.AuthKey; + + peer.PingTime = 5; + + peer.PeerConnected += Peer_PeerConnected; + + return peer; + } + + /// + /// Event action that handles when a peer connects. + /// + /// + /// + private static void Peer_PeerConnected(object sender, PeerConnectedEvent e) + { + //FnePeer peer = (FnePeer)sender; + //peer.SendMasterGroupAffiliation(1, (uint)Program.Configuration.DestinationId); + } + + /// + /// Helper to send a activity transfer message to the master. + /// + /// Message to send + public void SendActivityTransfer(string message) + { + /* stub */ + } + + /// + /// Helper to send a diagnostics transfer message to the master. + /// + /// Message to send + public void SendDiagnosticsTransfer(string message) + { + /* stub */ + } + } // public class PeerSystem +} // namespace rc2_dvm \ No newline at end of file diff --git a/WhackerLinkConsoleV2/SampleTimeConvert.cs b/WhackerLinkConsoleV2/SampleTimeConvert.cs new file mode 100644 index 0000000..56a6007 --- /dev/null +++ b/WhackerLinkConsoleV2/SampleTimeConvert.cs @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: AGPL-3.0-only +/** +* Digital Voice Modem - Audio Bridge +* AGPLv3 Open Source. Use is subject to license terms. +* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. +* +* @package DVM / Audio Bridge +* @license AGPLv3 License (https://opensource.org/licenses/AGPL-3.0) +* +* Copyright (C) 2023 Bryan Biedenkapp, N2PLL +* +*/ +using System; + +using NAudio.Wave; + +namespace WhackerLinkConsoleV2 +{ + /// + /// + /// + public class SampleTimeConvert + { + /* + ** Methods + */ + + /// + /// (ms) to sample count conversion + /// + /// Wave format + /// Number of milliseconds + /// Number of samples + public static int ToSamples(WaveFormat format, int ms) + { + return (int)(((long)ms) * format.SampleRate * format.Channels / 1000); + } + + /// + /// Sample count to (ms) conversion + /// + /// Wave format + /// Number of samples + /// Number of milliseconds + public static int ToMS(WaveFormat format, int samples) + { + return (int)(((float)samples / (float)format.SampleRate / (float)format.Channels) * 1000); + } + + /// + /// samples to bytes conversion + /// + /// Wave format + /// Number of samples + /// Number of bytes for the number of samples + public static int ToBytes(WaveFormat format, int samples) + { + return samples * (format.BitsPerSample / 8); + } + + /// + /// (ms) to bytes conversion + /// + /// Wave format + /// Number of milliseconds + /// Number of bytes for the amount of audio in (ms) + public static int MSToSampleBytes(WaveFormat format, int ms) + { + return ToBytes(format, ToSamples(format, ms)); + } + } // public class SamplesToMS +} // namespace dvmbridge \ No newline at end of file diff --git a/WhackerLinkConsoleV2/VocoderInterop.cs b/WhackerLinkConsoleV2/VocoderInterop.cs new file mode 100644 index 0000000..c177849 --- /dev/null +++ b/WhackerLinkConsoleV2/VocoderInterop.cs @@ -0,0 +1,397 @@ +// From W3AXL console + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using fnecore; +using Serilog; +using WhackerLinkLib; + +namespace WhackerLinkConsoleV2 +{ + + public enum MBE_MODE + { + DMR_AMBE, //! DMR AMBE + IMBE_88BIT, //! 88-bit IMBE (P25) + } + + /// + /// Wrapper class for the c++ dvmvocoder encoder library + /// + /// Using info from https://stackoverflow.com/a/315064/1842613 + public class MBEEncoder + { + /// + /// Create a new MBEEncoder + /// + /// + [DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr MBEEncoder_Create(MBE_MODE mode); + + /// + /// Encode PCM16 samples to MBE codeword + /// + /// Input PCM samples + /// Output MBE codeword + [DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)] + public static extern void MBEEncoder_Encode(IntPtr pEncoder, [In] Int16[] samples, [Out] byte[] codeword); + + /// + /// Encode MBE to bits + /// + /// + /// + /// + [DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)] + public static extern void MBEEncoder_EncodeBits(IntPtr pEncoder, [In] char[] bits, [Out] byte[] codeword); + + /// + /// Delete a created MBEEncoder + /// + /// + [DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)] + public static extern void MBEEncoder_Delete(IntPtr pEncoder); + + /// + /// Pointer to the encoder instance + /// + private IntPtr encoder { get; set; } + + /// + /// Create a new MBEEncoder instance + /// + /// Vocoder Mode (DMR or P25) + public MBEEncoder(MBE_MODE mode) + { + encoder = MBEEncoder_Create(mode); + } + + /// + /// Private class destructor properly deletes interop instance + /// + ~MBEEncoder() + { + MBEEncoder_Delete(encoder); + } + + /// + /// Encode PCM16 samples to MBE codeword + /// + /// + /// + public void encode([In] Int16[] samples, [Out] byte[] codeword) + { + MBEEncoder_Encode(encoder, samples, codeword); + } + + public void encodeBits([In] char[] bits, [Out] byte[] codeword) + { + MBEEncoder_EncodeBits(encoder, bits, codeword); + } + } + + /// + /// Wrapper class for the c++ dvmvocoder decoder library + /// + public class MBEDecoder + { + /// + /// Create a new MBEDecoder + /// + /// + [DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)] + public static extern IntPtr MBEDecoder_Create(MBE_MODE mode); + + /// + /// Decode MBE codeword to samples + /// + /// Input PCM samples + /// Output MBE codeword + /// Number of decode errors + [DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)] + public static extern Int32 MBEDecoder_Decode(IntPtr pDecoder, [In] byte[] codeword, [Out] Int16[] samples); + + /// + /// Decode MBE to bits + /// + /// + /// + /// + /// + [DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)] + public static extern Int32 MBEDecoder_DecodeBits(IntPtr pDecoder, [In] byte[] codeword, [Out] char[] bits); + + /// + /// Delete a created MBEDecoder + /// + /// + [DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)] + public static extern void MBEDecoder_Delete(IntPtr pDecoder); + + /// + /// Pointer to the decoder instance + /// + private IntPtr decoder { get; set; } + + /// + /// Create a new MBEDecoder instance + /// + /// Vocoder Mode (DMR or P25) + public MBEDecoder(MBE_MODE mode) + { + decoder = MBEDecoder_Create(mode); + } + + /// + /// Private class destructor properly deletes interop instance + /// + ~MBEDecoder() + { + MBEDecoder_Delete(decoder); + } + + /// + /// Decode MBE codeword to PCM16 samples + /// + /// + /// + public Int32 decode([In] byte[] codeword, [Out] Int16[] samples) + { + return MBEDecoder_Decode(decoder, codeword, samples); + } + + /// + /// Decode MBE codeword to bits + /// + /// + /// + /// + public Int32 decodeBits([In] byte[] codeword, [Out] char[] bits) + { + return MBEDecoder_DecodeBits(decoder, codeword, bits); + } + } + + public static class MBEToneGenerator + { + /// + /// Encodes a single tone to an AMBE tone frame + /// + /// + /// + /// + /// + public static void AmbeEncodeSingleTone(int tone_freq_hz, char tone_amplitude, [Out] byte[] codeword) + { + // U bit vectors + // u0 and u1 are 12 bits + // u2 is 11 bits + // u3 is 14 bits + // total length is 49 bits + ushort[] u = new ushort[4]; + + // Convert the tone frequency to the nearest tone index + uint tone_idx = (uint)((float)tone_freq_hz / 31.25f); + + // Validate tone index + if (tone_idx < 5 || tone_idx > 122) + { + throw new ArgumentOutOfRangeException($"Tone index for frequency out of range!"); + } + + // Validate amplitude value + if (tone_amplitude > 127) + { + throw new ArgumentOutOfRangeException("Tone amplitude must be between 0 and 127!"); + } + + // Make sure tone index only has 7 bits (it should but we make sure :) ) + tone_idx &= 0b01111111; + + // Encode u vectors per TIA-102.BABA-1 section 7.2 + + // u0[11-6] are always 1 to indicate a tone, so we left-shift 63u (0x00111111) a full byte (8 bits) + u[0] |= (ushort)(63 << 8); + + // u0[5-0] are AD (tone amplitude byte) bits 6-1 + u[0] |= (ushort)(tone_amplitude >> 1); + + // u1[11-4] are tone index bits 7-0 (the full byte) + u[1] |= (ushort)(tone_idx << 4); + + // u1[3-0] are tone index bits 7-4 + u[1] |= (ushort)(tone_idx >> 4); + + // u2[10-7] are tone index bits 3-0 + u[2] |= (ushort)((tone_idx & 0b00001111) << 7); + + // u2[6-0] are tone index bits 7-1 + u[2] |= (ushort)(tone_idx >> 1); + + // u3[13] is the last bit of the tone index + u[3] |= (ushort)((tone_idx & 0b1) << 13); + + // u3[12-5] is the full tone index byte + u[3] |= (ushort)(tone_idx << 5); + + // u3[4] is the last bit of the amplitude byte + u[3] |= (ushort)((tone_amplitude & 0b1) << 4); + + // u3[3-0] is always 0 so we don't have to do anything here + + // Convert u buffer to byte + Buffer.BlockCopy(u, 0, codeword, 0, 8); + } + + /// + /// Encode a single tone to an IMBE codeword sequence using a lookup table + /// + /// + /// + public static void IMBEEncodeSingleTone(ushort tone_freq_hz, [Out] byte[] codeword) + { + // Find nearest tone in the lookup table + List tone_keys = VocoderToneLookupTable.IMBEToneFrames.Keys.ToList(); + ushort nearest = tone_keys.Aggregate((x, y) => Math.Abs(x - tone_freq_hz) < Math.Abs(y - tone_freq_hz) ? x : y); + byte[] tone_codeword = VocoderToneLookupTable.IMBEToneFrames[nearest]; + Array.Copy(tone_codeword, codeword, tone_codeword.Length); + } + } + + public class MBEInterleaver + { + public const int PCM_SAMPLES = 160; + public const int AMBE_CODEWORD_SAMPLES = 9; + public const int AMBE_CODEWORD_BITS = 49; + public const int IMBE_CODEWORD_SAMPLES = 11; + public const int IMBE_CODEWORD_BITS = 88; + + private MBE_MODE mode; + + private MBEEncoder encoder; + private MBEDecoder decoder; + + public MBEInterleaver(MBE_MODE mode) + { + this.mode = mode; + encoder = new MBEEncoder(this.mode); + decoder = new MBEDecoder(this.mode); + } + + public Int32 Decode([In] byte[] codeword, [Out] byte[] mbeBits) + { + // Input validation + if (codeword == null) + { + throw new NullReferenceException("Input MBE codeword is null!"); + } + + char[] bits = null; + + // Set up based on mode + if (mode == MBE_MODE.DMR_AMBE) + { + if (codeword.Length != AMBE_CODEWORD_SAMPLES) + { + throw new ArgumentOutOfRangeException($"AMBE codeword length is != {AMBE_CODEWORD_SAMPLES}"); + } + bits = new char[AMBE_CODEWORD_BITS]; + } + else if (mode == MBE_MODE.IMBE_88BIT) + { + if (codeword.Length != IMBE_CODEWORD_SAMPLES) + { + throw new ArgumentOutOfRangeException($"IMBE codeword length is != {IMBE_CODEWORD_SAMPLES}"); + } + bits = new char[IMBE_CODEWORD_BITS]; + } + + if (bits == null) + { + throw new NullReferenceException("Failed to initialize decoder"); + } + + // Decode + int errs = decoder.decodeBits(codeword, bits); + + // Copy + if (mode == MBE_MODE.DMR_AMBE) + { + // Copy bits + mbeBits = new byte[AMBE_CODEWORD_BITS]; + Array.Copy(bits, mbeBits, AMBE_CODEWORD_BITS); + + } + else if (mode == MBE_MODE.IMBE_88BIT) + { + // Copy bits + mbeBits = new byte[IMBE_CODEWORD_BITS]; + Array.Copy(bits, mbeBits, IMBE_CODEWORD_BITS); + } + + return errs; + } + + public void Encode([In] byte[] mbeBits, [Out] byte[] codeword) + { + if (mbeBits == null) + { + throw new NullReferenceException("Input MBE bit array is null!"); + } + + char[] bits = null; + + // Set up based on mode + if (mode == MBE_MODE.DMR_AMBE) + { + if (mbeBits.Length != AMBE_CODEWORD_BITS) + { + throw new ArgumentOutOfRangeException($"AMBE codeword bit length is != {AMBE_CODEWORD_BITS}"); + } + bits = new char[AMBE_CODEWORD_BITS]; + Array.Copy(mbeBits, bits, AMBE_CODEWORD_BITS); + } + else if (mode == MBE_MODE.IMBE_88BIT) + { + if (mbeBits.Length != IMBE_CODEWORD_BITS) + { + throw new ArgumentOutOfRangeException($"IMBE codeword bit length is != {AMBE_CODEWORD_BITS}"); + } + bits = new char[IMBE_CODEWORD_BITS]; + Array.Copy(mbeBits, bits, IMBE_CODEWORD_BITS); + } + + if (bits == null) + { + throw new ArgumentException("Bit array did not get set up properly!"); + } + + // Encode samples + if (mode == MBE_MODE.DMR_AMBE) + { + // Create output array + byte[] codewords = new byte[AMBE_CODEWORD_SAMPLES]; + // Encode + encoder.encodeBits(bits, codewords); + // Copy + codeword = new byte[AMBE_CODEWORD_SAMPLES]; + Array.Copy(codewords, codeword, IMBE_CODEWORD_SAMPLES); + } + else if (mode == MBE_MODE.IMBE_88BIT) + { + // Create output array + byte[] codewords = new byte[IMBE_CODEWORD_SAMPLES]; + // Encode + encoder.encodeBits(bits, codewords); + // Copy + codeword = new byte[IMBE_CODEWORD_SAMPLES]; + Array.Copy(codewords, codeword, IMBE_CODEWORD_SAMPLES); + } + } + } +} \ No newline at end of file diff --git a/WhackerLinkConsoleV2/WhackerLinkConsoleV2.csproj b/WhackerLinkConsoleV2/WhackerLinkConsoleV2.csproj index a6a2f57..68aabbb 100644 --- a/WhackerLinkConsoleV2/WhackerLinkConsoleV2.csproj +++ b/WhackerLinkConsoleV2/WhackerLinkConsoleV2.csproj @@ -6,7 +6,9 @@ disable enable true - AnyCPU;x64 + AnyCPU;x64;x86 + Debug;Release;WIN32 + true @@ -50,6 +52,7 @@ + @@ -69,6 +72,24 @@ + + + Always + + + Always + + + Always + + + Always + + + Always + + + diff --git a/fnecore b/fnecore new file mode 160000 index 0000000..df87bd1 --- /dev/null +++ b/fnecore @@ -0,0 +1 @@ +Subproject commit df87bd1ea5a944a272a979469b84e4b5f5f8d108