You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
451 lines
17 KiB
451 lines
17 KiB
// 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.Runtime.InteropServices;
|
|
|
|
namespace dvmconsole
|
|
{
|
|
/// <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
|
|
}
|