Add initial support for DVM FNE

pull/1/head
firealarmss 11 months ago
parent 90c89bd381
commit 9e047e9921

3
.gitmodules vendored

@ -1,3 +1,6 @@
[submodule "WhackerLinkLib"]
path = WhackerLinkLib
url = https://github.com/whackerlink/WhackerLinkLib
[submodule "fnecore"]
path = fnecore
url = https://github.com/dvmproject/fnecore

@ -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

@ -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
{
/// <summary>
/// Implements P/Invoke to callback into external AMBE encoder/decoder library.
/// </summary>
/// <remarks>This is used to interface to a external library that talks to a DVSI USB-3000.</remarks>
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;
/// <summary>
///
/// </summary>
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
*/
/// <summary>
/// Gets the currently operating decoder mode.
/// </summary>
public AmbeMode DecoderMode
{
get
{
unsafe
{
fixed (byte* state = decoderState)
return (AmbeMode)ambe_get_dec_mode((IntPtr)state);
}
}
}
/// <summary>
/// Gets the currently operating encoder mode.
/// </summary>
public AmbeMode EncoderMode
{
get
{
unsafe
{
fixed (byte* state = encoderState)
return (AmbeMode)ambe_get_enc_mode((IntPtr)state);
}
}
}
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="AmbeVocoder"/> class.
/// </summary>
/// <param name="fullRate"></param>
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);
}
}
/// <summary>
/// Helper to unpack the codeword bytes into codeword bits for use with the AMBE decoder.
/// </summary>
/// <remarks><see cref="short"/> output bits array.</remarks>
/// <param name="codewordBits">Codeword bits.</param>
/// <param name="codeword">Codeword bytes.</param>
/// <param name="lengthBytes">Length of codeword in bytes.</param>
/// <param name="lengthBits">Length of codeword in bits.</param>
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++;
}
}
/// <summary>
/// Helper to unpack the codeword bytes into codeword bits for use with the AMBE decoder.
/// </summary>
/// <remarks><see cref="byte"/> output bits array.</remarks>
/// <param name="codewordBits">Codeword bits.</param>
/// <param name="codeword">Codeword bytes.</param>
/// <param name="lengthBytes">Length of codeword in bytes.</param>
/// <param name="lengthBits">Length of codeword in bits.</param>
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++;
}
}
/// <summary>
/// Decodes the given MBE codewords to PCM samples using the decoder mode.
/// </summary>
/// <param name="codeword"></param>
/// <param name="samples"></param>
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;
}
/// <summary>
/// Calls ambe_init_dec() in the external DLL.
/// </summary>
/// <param name="state">Buffer containing the decoder state to initialize.</param>
/// <param name="mode">AMBE mode; FULL (0) or HALF (1).</param>
[DllImport("AMBE.DLL", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern void ambe_init_dec([Out] IntPtr state, [In] short mode);
/// <summary>
/// Calls ambe_get_dec_mode() in the external DLL.
/// </summary>
/// <param name="state">Buffer containing the decoder state to initialize.</param>
[DllImport("AMBE.DLL", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern short ambe_get_dec_mode([In] IntPtr state);
/// <summary>
/// Calls ambe_voice_dec() in the external DLL.
/// </summary>
/// <param name="samples"></param>
/// <param name="sampleLength"></param>
/// <param name="codeword"></param>
/// <param name="bitSteal"></param>
/// <param name="cmode"></param>
/// <param name="n"></param>
/// <param name="state">Buffer containing the decoder state.</param>
/// <returns></returns>
[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);
/// <summary>
/// Helper to pack the codeword bits into codeword bytes for use with the AMBE encoder.
/// </summary>
/// <remarks><see cref="short"/> input bits array.</remarks>
/// <param name="codewordBits">Codeword bits.</param>
/// <param name="codeword">Codeword bytes.</param>
/// <param name="lengthBytes">Length of codeword in bytes.</param>
/// <param name="lengthBits">Length of codeword in bits.</param>
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++;
}
}
/// <summary>
/// Helper to pack the codeword bits into codeword bytes for use with the AMBE encoder.
/// </summary>
/// <remarks><see cref="byte"/> input bits array.</remarks>
/// <param name="codewordBits">Codeword bits.</param>
/// <param name="codeword">Codeword bytes.</param>
/// <param name="lengthBytes">Length of codeword in bytes.</param>
/// <param name="lengthBits">Length of codeword in bits.</param>
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++;
}
}
/// <summary>
/// Encodes the given PCM samples using the encoder mode to MBE codewords.
/// </summary>
/// <param name="samples"></param>
/// <param name="codeword"></param>
/// <param name="encodeDMR"></param>
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);
}
}
/// <summary>
/// Calls ambe_init_enc() in the external DLL.
/// </summary>
/// <param name="state">Buffer containing the encoder state to initialize.</param>
/// <param name="mode">AMBE mode; FULL (0) or HALF (1).</param>
/// <param name="initialize">Flag to initialize encoder state fully, 1 to initialize, 0 to not.</param>
[DllImport("AMBE.DLL", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern void ambe_init_enc([Out] IntPtr state, [In] short mode, [In] short initialize);
/// <summary>
/// Calls ambe_get_enc_mode() in the external DLL.
/// </summary>
/// <param name="state">Buffer containing the encoder state to initialize.</param>
[DllImport("AMBE.DLL", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern short ambe_get_enc_mode([In] IntPtr state);
/// <summary>
/// Calls ambe_voice_enc() in the external DLL.
/// </summary>
/// <param name="codeword"></param>
/// <param name="bitSteal"></param>
/// <param name="samples"></param>
/// <param name="sampleLength"></param>
/// <param name="cmode"></param>
/// <param name="n"></param>
/// <param name="unk"></param>
/// <param name="state">Buffer containing the encoder state.</param>
/// <returns></returns>
[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

@ -14,15 +14,17 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* 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<byte[]> chunkedPcm = new List<byte[]>();
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);
}

@ -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
{
/// <summary>
/// Implements a FNE system base.
/// </summary>
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
*/
/// <summary>
/// Callback used to validate incoming DMR data.
/// </summary>
/// <param name="peerId">Peer ID</param>
/// <param name="srcId">Source Address</param>
/// <param name="dstId">Destination Address</param>
/// <param name="slot">Slot Number</param>
/// <param name="callType">Call Type (Group or Private)</param>
/// <param name="frameType">Frame Type</param>
/// <param name="dataType">DMR Data Type</param>
/// <param name="streamId">Stream ID</param>
/// <param name="message">Raw message data</param>
/// <returns>True, if data stream is valid, otherwise false.</returns>
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;
}
/// <summary>
/// Creates an DMR frame message.
/// </summary>
/// <param name="data"></param>
/// <param name="srcId"></param>
/// <param name="dstId"></param>
/// <param name="slot"></param>
/// <param name="frameType"></param>
/// <param name="seqNo"></param>
/// <param name="n"></param>
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);
}
/// <summary>
/// Helper to send a DMR terminator with LC message.
/// </summary>
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);
}
/// <summary>
/// Event handler used to process incoming DMR data.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected override void DMRDataReceived(object sender, DMRDataReceivedEvent e)
{
return;
}
} // public abstract partial class FneSystemBase : fnecore.FneSystemBase
}

@ -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
{
/// <summary>
/// Implements a FNE system base.
/// </summary>
public abstract partial class FneSystemBase : fnecore.FneSystemBase
{
private List<Tuple<byte[], ushort>> nxdnCallData = new List<Tuple<byte[], ushort>>();
/*
** Methods
*/
/// <summary>
/// Callback used to validate incoming NXDN data.
/// </summary>
/// <param name="peerId">Peer ID</param>
/// <param name="srcId">Source Address</param>
/// <param name="dstId">Destination Address</param>
/// <param name="callType">Call Type (Group or Private)</param>
/// <param name="messageType">NXDN Message Type</param>
/// <param name="frameType">Frame Type</param>
/// <param name="streamId">Stream ID</param>
/// <param name="message">Raw message data</param>
/// <returns>True, if data stream is valid, otherwise false.</returns>
protected override bool NXDNDataValidate(uint peerId, uint srcId, uint dstId, CallType callType, NXDNMessageType messageType, FrameType frameType, uint streamId, byte[] message)
{
return true;
}
/// <summary>
/// Event handler used to process incoming NXDN data.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected override void NXDNDataReceived(object sender, NXDNDataReceivedEvent e)
{
return;
}
} // public abstract partial class FneSystemBase : fnecore.FneSystemBase
}

@ -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
{
/// <summary>
/// Implements a FNE system base.
/// </summary>
public abstract partial class FneSystemBase : fnecore.FneSystemBase
{
public const int IMBE_BUF_LEN = 11;
/*
** Methods
*/
/// <summary>
/// Callback used to validate incoming P25 data.
/// </summary>
/// <param name="peerId">Peer ID</param>
/// <param name="srcId">Source Address</param>
/// <param name="dstId">Destination Address</param>
/// <param name="callType">Call Type (Group or Private)</param>
/// <param name="duid">P25 DUID</param>
/// <param name="frameType">Frame Type</param>
/// <param name="streamId">Stream ID</param>
/// <param name="message">Raw message data</param>
/// <returns>True, if data stream is valid, otherwise false.</returns>
protected override bool P25DataValidate(uint peerId, uint srcId, uint dstId, CallType callType, P25DUID duid, FrameType frameType, uint streamId, byte[] message)
{
return true;
}
/// <summary>
/// Event handler used to pre-process incoming P25 data.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected override void P25DataPreprocess(object sender, P25DataReceivedEvent e)
{
return;
}
public void CreateNewP25MessageHdr(byte duid, RemoteCallData callData, ref byte[] data)
{
CreateP25MessageHdr(duid, callData, ref data);
}
/// <summary>
/// Helper to send a P25 TDU message.
/// </summary>
/// <param name="grantDemand"></param>
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);
}
/// <summary>
/// Encode a logical link data unit 1.
/// </summary>
/// <param name="data"></param>
/// <param name="offset"></param>
/// <param name="imbe"></param>
/// <param name="frameType"></param>
/// <param name="srcIdOverride"></param>
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);
}
/// <summary>
/// Creates an P25 LDU1 frame message.
/// </summary>
/// <param name="data"></param>
/// <param name="srcId"></param>
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;
}
/// <summary>
/// Encode a logical link data unit 2.
/// </summary>
/// <param name="data"></param>
/// <param name="offset"></param>
/// <param name="imbe"></param>
/// <param name="frameType"></param>
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);
}
/// <summary>
/// Creates an P25 LDU2 frame message.
/// </summary>
/// <param name="netLDU2">Input LDU data array</param>
/// <param name="data">Output data array</param>
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;
}
/// <summary>
/// Event handler used to process incoming P25 data.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
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
}

@ -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
{
/// <summary>
/// Represents the individual timeslot data status.
/// </summary>
public class SlotStatus
{
/// <summary>
/// Rx Start Time
/// </summary>
public DateTime RxStart = DateTime.Now;
/// <summary>
///
/// </summary>
public uint RxSeq = 0;
/// <summary>
/// Rx RF Source
/// </summary>
public uint RxRFS = 0;
/// <summary>
/// Tx RF Source
/// </summary>
public uint TxRFS = 0;
/// <summary>
/// Rx Stream ID
/// </summary>
public uint RxStreamId = 0;
/// <summary>
/// Tx Stream ID
/// </summary>
public uint TxStreamId = 0;
/// <summary>
/// Rx TG ID
/// </summary>
public uint RxTGId = 0;
/// <summary>
/// Tx TG ID
/// </summary>
public uint TxTGId = 0;
/// <summary>
/// Tx Privacy TG ID
/// </summary>
public uint TxPITGId = 0;
/// <summary>
/// Rx Time
/// </summary>
public DateTime RxTime = DateTime.Now;
/// <summary>
/// Tx Time
/// </summary>
public DateTime TxTime = DateTime.Now;
/// <summary>
/// Rx Type
/// </summary>
public FrameType RxType = FrameType.TERMINATOR;
/** DMR Data */
/// <summary>
/// Rx Link Control Header
/// </summary>
public LC DMR_RxLC = null;
/// <summary>
/// Rx Privacy Indicator Link Control Header
/// </summary>
public PrivacyLC DMR_RxPILC = null;
/// <summary>
/// Tx Link Control Header
/// </summary>
public LC DMR_TxHLC = null;
/// <summary>
/// Tx Privacy Link Control Header
/// </summary>
public PrivacyLC DMR_TxPILC = null;
/// <summary>
/// Tx Terminator Link Control
/// </summary>
public LC DMR_TxTLC = null;
} // public class SlotStatus
/// <summary>
/// Implements a FNE system.
/// </summary>
public abstract partial class FneSystemBase : fnecore.FneSystemBase
{
public List<byte[]> processedChunks = new List<byte[]>();
private Random rand;
internal MainWindow mainWindow;
// List of active calls
private List<(uint, byte)> activeTalkgroups = new List<(uint, byte)>();
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="FneSystemBase"/> class.
/// </summary>
/// <param name="fne">Instance of <see cref="FneMaster"/> or <see cref="FnePeer"/></param>
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;
}
};
}
/// <summary>
/// Stops the main execution loop for this <see cref="FneSystemBase"/>.
/// </summary>
public override void Stop()
{
base.Stop();
}
/// <summary>
/// Callback used to process whether or not a peer is being ignored for traffic.
/// </summary>
/// <param name="peerId">Peer ID</param>
/// <param name="srcId">Source Address</param>
/// <param name="dstId">Destination Address</param>
/// <param name="slot">Slot Number</param>
/// <param name="callType">Call Type (Group or Private)</param>
/// <param name="frameType">Frame Type</param>
/// <param name="dataType">DMR Data Type</param>
/// <param name="streamId">Stream ID</param>
/// <returns>True, if peer is ignored, otherwise false.</returns>
protected override bool PeerIgnored(uint peerId, uint srcId, uint dstId, byte slot, fnecore.CallType callType, FrameType frameType, DMRDataType dataType, uint streamId)
{
return false;
}
/// <summary>
/// Event handler used to handle a peer connected event.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected override void PeerConnected(object sender, PeerConnectedEvent e)
{
return;
}
/// <summary>
/// Returns a new stream ID
/// </summary>
/// <returns></returns>
public uint NewStreamId()
{
return (uint)rand.Next(int.MinValue, int.MaxValue);
}
} // public abstract partial class FneSystemBase : fnecore.FneSystemBase
}

@ -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 <http://www.gnu.org/licenses/>.
*
* Copyright (C) 2024-2025 Caleb, K4PHP
*
*/
using WhackerLinkLib.Models.Radio;
using WhackerLinkLib.Network;
namespace WhackerLinkConsoleV2
{
/// <summary>
/// WhackerLink peer/client websocket manager for having multiple systems
/// </summary>
public class FneSystemManager
{
private readonly Dictionary<string, PeerSystem> _webSocketHandlers;
/// <summary>
/// Creates an instance of <see cref="PeerSystem"/>
/// </summary>
public FneSystemManager()
{
_webSocketHandlers = new Dictionary<string, PeerSystem>();
}
/// <summary>
/// Create a new <see cref="PeerSystem"/> for a new system
/// </summary>
/// <param name="systemId"></param>
public void AddFneSystem(string systemId, Codeplug.System system, MainWindow mainWindow)
{
if (!_webSocketHandlers.ContainsKey(systemId))
{
_webSocketHandlers[systemId] = new PeerSystem(mainWindow, system);
}
}
/// <summary>
/// Return a <see cref="PeerSystem"/> by looking up a systemid
/// </summary>
/// <param name="systemId"></param>
/// <returns></returns>
/// <exception cref="KeyNotFoundException"></exception>
public PeerSystem GetFneSystem(string systemId)
{
if (_webSocketHandlers.TryGetValue(systemId, out var handler))
{
return handler;
}
throw new KeyNotFoundException($"WebSocketHandler for system '{systemId}' not found.");
}
/// <summary>
/// Delete a <see cref="Peer"/> by system id
/// </summary>
/// <param name="systemId"></param>
public void RemoveFneSystem(string systemId)
{
if (_webSocketHandlers.TryGetValue(systemId, out var handler))
{
handler.peer.Stop();
_webSocketHandlers.Remove(systemId);
}
}
/// <summary>
/// Check if the manager has a handler
/// </summary>
/// <param name="systemId"></param>
/// <returns></returns>
public bool HasFneSystem(string systemId)
{
return _webSocketHandlers.ContainsKey(systemId);
}
/// <summary>
/// Cleanup
/// </summary>
public void ClearAll()
{
foreach (var handler in _webSocketHandlers.Values)
{
handler.peer.Stop();
}
_webSocketHandlers.Clear();
}
}
}

File diff suppressed because it is too large Load Diff

@ -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<ushort, KeyInfo> keys = new Dictionary<ushort, KeyInfo>();
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;
}
}
}
}

@ -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
{
/// <summary>
/// Implements a peer FNE router system.
/// </summary>
public class PeerSystem : FneSystemBase
{
public FnePeer peer;
/*
** Methods
*/
/// <summary>
/// Initializes a new instance of the <see cref="PeerSystem"/> class.
/// </summary>
public PeerSystem(MainWindow mainWindow, Codeplug.System system) : base(Create(system), mainWindow)
{
peer = (FnePeer)fne;
}
/// <summary>
/// Internal helper to instantiate a new instance of <see cref="FnePeer"/> class.
/// </summary>
/// <returns><see cref="FnePeer"/></returns>
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;
}
/// <summary>
/// Event action that handles when a peer connects.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private static void Peer_PeerConnected(object sender, PeerConnectedEvent e)
{
//FnePeer peer = (FnePeer)sender;
//peer.SendMasterGroupAffiliation(1, (uint)Program.Configuration.DestinationId);
}
/// <summary>
/// Helper to send a activity transfer message to the master.
/// </summary>
/// <param name="message">Message to send</param>
public void SendActivityTransfer(string message)
{
/* stub */
}
/// <summary>
/// Helper to send a diagnostics transfer message to the master.
/// </summary>
/// <param name="message">Message to send</param>
public void SendDiagnosticsTransfer(string message)
{
/* stub */
}
} // public class PeerSystem
} // namespace rc2_dvm

@ -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
{
/// <summary>
///
/// </summary>
public class SampleTimeConvert
{
/*
** Methods
*/
/// <summary>
/// (ms) to sample count conversion
/// </summary>
/// <param name="format">Wave format</param>
/// <param name="ms">Number of milliseconds</param>
/// <returns>Number of samples</returns>
public static int ToSamples(WaveFormat format, int ms)
{
return (int)(((long)ms) * format.SampleRate * format.Channels / 1000);
}
/// <summary>
/// Sample count to (ms) conversion
/// </summary>
/// <param name="format">Wave format</param>
/// <param name="samples">Number of samples</param>
/// <returns>Number of milliseconds</returns>
public static int ToMS(WaveFormat format, int samples)
{
return (int)(((float)samples / (float)format.SampleRate / (float)format.Channels) * 1000);
}
/// <summary>
/// samples to bytes conversion
/// </summary>
/// <param name="format">Wave format</param>
/// <param name="samples">Number of samples</param>
/// <returns>Number of bytes for the number of samples</returns>
public static int ToBytes(WaveFormat format, int samples)
{
return samples * (format.BitsPerSample / 8);
}
/// <summary>
/// (ms) to bytes conversion
/// </summary>
/// <param name="format">Wave format</param>
/// <param name="ms">Number of milliseconds</param>
/// <returns>Number of bytes for the amount of audio in (ms)</returns>
public static int MSToSampleBytes(WaveFormat format, int ms)
{
return ToBytes(format, ToSamples(format, ms));
}
} // public class SamplesToMS
} // namespace dvmbridge

@ -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)
}
/// <summary>
/// Wrapper class for the c++ dvmvocoder encoder library
/// </summary>
/// Using info from https://stackoverflow.com/a/315064/1842613
public class MBEEncoder
{
/// <summary>
/// Create a new MBEEncoder
/// </summary>
/// <returns></returns>
[DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr MBEEncoder_Create(MBE_MODE mode);
/// <summary>
/// Encode PCM16 samples to MBE codeword
/// </summary>
/// <param name="samples">Input PCM samples</param>
/// <param name="codeword">Output MBE codeword</param>
[DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)]
public static extern void MBEEncoder_Encode(IntPtr pEncoder, [In] Int16[] samples, [Out] byte[] codeword);
/// <summary>
/// Encode MBE to bits
/// </summary>
/// <param name="pEncoder"></param>
/// <param name="bits"></param>
/// <param name="codeword"></param>
[DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)]
public static extern void MBEEncoder_EncodeBits(IntPtr pEncoder, [In] char[] bits, [Out] byte[] codeword);
/// <summary>
/// Delete a created MBEEncoder
/// </summary>
/// <param name="pEncoder"></param>
[DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)]
public static extern void MBEEncoder_Delete(IntPtr pEncoder);
/// <summary>
/// Pointer to the encoder instance
/// </summary>
private IntPtr encoder { get; set; }
/// <summary>
/// Create a new MBEEncoder instance
/// </summary>
/// <param name="mode">Vocoder Mode (DMR or P25)</param>
public MBEEncoder(MBE_MODE mode)
{
encoder = MBEEncoder_Create(mode);
}
/// <summary>
/// Private class destructor properly deletes interop instance
/// </summary>
~MBEEncoder()
{
MBEEncoder_Delete(encoder);
}
/// <summary>
/// Encode PCM16 samples to MBE codeword
/// </summary>
/// <param name="samples"></param>
/// <param name="codeword"></param>
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);
}
}
/// <summary>
/// Wrapper class for the c++ dvmvocoder decoder library
/// </summary>
public class MBEDecoder
{
/// <summary>
/// Create a new MBEDecoder
/// </summary>
/// <returns></returns>
[DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr MBEDecoder_Create(MBE_MODE mode);
/// <summary>
/// Decode MBE codeword to samples
/// </summary>
/// <param name="samples">Input PCM samples</param>
/// <param name="codeword">Output MBE codeword</param>
/// <returns>Number of decode errors</returns>
[DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)]
public static extern Int32 MBEDecoder_Decode(IntPtr pDecoder, [In] byte[] codeword, [Out] Int16[] samples);
/// <summary>
/// Decode MBE to bits
/// </summary>
/// <param name="pDecoder"></param>
/// <param name="codeword"></param>
/// <param name="mbeBits"></param>
/// <returns></returns>
[DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)]
public static extern Int32 MBEDecoder_DecodeBits(IntPtr pDecoder, [In] byte[] codeword, [Out] char[] bits);
/// <summary>
/// Delete a created MBEDecoder
/// </summary>
/// <param name="pDecoder"></param>
[DllImport("libvocoder", CallingConvention = CallingConvention.Cdecl)]
public static extern void MBEDecoder_Delete(IntPtr pDecoder);
/// <summary>
/// Pointer to the decoder instance
/// </summary>
private IntPtr decoder { get; set; }
/// <summary>
/// Create a new MBEDecoder instance
/// </summary>
/// <param name="mode">Vocoder Mode (DMR or P25)</param>
public MBEDecoder(MBE_MODE mode)
{
decoder = MBEDecoder_Create(mode);
}
/// <summary>
/// Private class destructor properly deletes interop instance
/// </summary>
~MBEDecoder()
{
MBEDecoder_Delete(decoder);
}
/// <summary>
/// Decode MBE codeword to PCM16 samples
/// </summary>
/// <param name="samples"></param>
/// <param name="codeword"></param>
public Int32 decode([In] byte[] codeword, [Out] Int16[] samples)
{
return MBEDecoder_Decode(decoder, codeword, samples);
}
/// <summary>
/// Decode MBE codeword to bits
/// </summary>
/// <param name="codeword"></param>
/// <param name="bits"></param>
/// <returns></returns>
public Int32 decodeBits([In] byte[] codeword, [Out] char[] bits)
{
return MBEDecoder_DecodeBits(decoder, codeword, bits);
}
}
public static class MBEToneGenerator
{
/// <summary>
/// Encodes a single tone to an AMBE tone frame
/// </summary>
/// <param name="tone_freq_hz"></param>
/// <param name="tone_amplitude"></param>
/// <param name="codeword"></param>
/// <exception cref="ArgumentOutOfRangeException"></exception>
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);
}
/// <summary>
/// Encode a single tone to an IMBE codeword sequence using a lookup table
/// </summary>
/// <param name="tone_freq_hz"></param>
/// <param name="codeword"></param>
public static void IMBEEncodeSingleTone(ushort tone_freq_hz, [Out] byte[] codeword)
{
// Find nearest tone in the lookup table
List<ushort> 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);
}
}
}
}

@ -6,7 +6,9 @@
<Nullable>disable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<Platforms>AnyCPU;x64</Platforms>
<Platforms>AnyCPU;x64;x86</Platforms>
<Configurations>Debug;Release;WIN32</Configurations>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
@ -50,6 +52,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\fnecore\fnecore.csproj" />
<ProjectReference Include="..\WhackerLinkLib\WhackerLinkLib.csproj" />
</ItemGroup>
@ -69,6 +72,24 @@
<Resource Include="Assets\WhackerLinkLogoV4.png" />
</ItemGroup>
<ItemGroup>
<None Update="Audio\alert1.wav">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Audio\alert2.wav">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Audio\alert3.wav">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Audio\emergency.wav">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="Audio\hold.wav">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<Page Update="ChannelBox.xaml">
<CopyToOutputDirectory></CopyToOutputDirectory>

@ -0,0 +1 @@
Subproject commit df87bd1ea5a944a272a979469b84e4b5f5f8d108
Loading…
Cancel
Save

Powered by TurnKey Linux.