From 57018fa67ab6448909195b6c627173c30ccaf09d Mon Sep 17 00:00:00 2001 From: yedajiang44 <602830483@qq.com> Date: Fri, 3 Jan 2020 09:30:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E9=9F=B3=E9=A2=91=E7=BC=96?= =?UTF-8?q?=E8=A7=A3=E7=A0=81=E4=BB=A5=E5=8F=8Aflv=E9=9F=B3=E9=A2=91tag?= =?UTF-8?q?=E5=B0=81=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/JT1078.Flv/Audio/AdpcmCodec.cs | 265 ++++++++++++++++++ src/JT1078.Flv/Audio/FaacEncoder.cs | 195 +++++++++++++ src/JT1078.Flv/Audio/G711ACodec.cs | 123 ++++++++ src/JT1078.Flv/Audio/G711UCodec.cs | 112 ++++++++ src/JT1078.Flv/Enums/AACPacketType.cs | 21 ++ src/JT1078.Flv/Enums/AudioFormat.cs | 61 ++++ src/JT1078.Flv/Enums/ChannelType.cs | 21 ++ src/JT1078.Flv/Enums/SampleBit.cs | 21 ++ .../Extensions/InteropExtensions.cs | 95 +++++++ src/JT1078.Flv/FlvEncoder.cs | 28 +- src/JT1078.Flv/FlvTags.cs | 4 + src/JT1078.Flv/JT1078.Flv.csproj | 5 + src/JT1078.Flv/JT1078.Flv.xml | 219 +++++++++++++++ src/JT1078.Flv/Libs/libfaac.dll | Bin 0 -> 71168 bytes src/JT1078.Flv/Metadata/AacPacke.cs | 32 +++ .../Metadata/AudioSpecificConfig.cs | 52 ++++ src/JT1078.Flv/Metadata/AudioTags.cs | 53 ++++ 17 files changed, 1298 insertions(+), 9 deletions(-) create mode 100644 src/JT1078.Flv/Audio/AdpcmCodec.cs create mode 100644 src/JT1078.Flv/Audio/FaacEncoder.cs create mode 100644 src/JT1078.Flv/Audio/G711ACodec.cs create mode 100644 src/JT1078.Flv/Audio/G711UCodec.cs create mode 100644 src/JT1078.Flv/Enums/AACPacketType.cs create mode 100644 src/JT1078.Flv/Enums/AudioFormat.cs create mode 100644 src/JT1078.Flv/Enums/ChannelType.cs create mode 100644 src/JT1078.Flv/Enums/SampleBit.cs create mode 100644 src/JT1078.Flv/Extensions/InteropExtensions.cs create mode 100644 src/JT1078.Flv/Libs/libfaac.dll create mode 100644 src/JT1078.Flv/Metadata/AacPacke.cs create mode 100644 src/JT1078.Flv/Metadata/AudioSpecificConfig.cs create mode 100644 src/JT1078.Flv/Metadata/AudioTags.cs diff --git a/src/JT1078.Flv/Audio/AdpcmCodec.cs b/src/JT1078.Flv/Audio/AdpcmCodec.cs new file mode 100644 index 0000000..cc325d9 --- /dev/null +++ b/src/JT1078.Flv/Audio/AdpcmCodec.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace JT1078.Flv.Audio +{ + public class State + { + /// + /// 上一个采样数据,当index为0是该值应该为未压缩的原数据 + /// + public short Valprev { get; set; } + + /// + /// 保留数据(未使用) + /// + public byte Reserved { get; set; } + + /// + /// 上一个block最后一个index,第一个block的index=0 + /// + public byte Index { get; set; } + } + public class AdpcmCodec + { + static readonly int[] indexTable = { + -1, -1, -1, -1, 2, 4, 6, 8, + -1, -1, -1, -1, 2, 4, 6, 8 + }; + + static readonly int[] stepsizeTable = { + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, + 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, + 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, + 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, + 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, + 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, + 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, + 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, + 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767 + }; + public static byte[] ToAdpcm(short[] indata, State state) + { + int val; /* Current input sample value */ + int sign; /* Current adpcm sign bit */ + int delta; /* Current adpcm output value */ + int diff; /* Difference between val and valprev */ + int step; /* Stepsize */ + int valpred; /* Predicted output value */ + int vpdiff; /* Current change to valpred */ + int index; /* Current step change index */ + int outputbuffer = 0; /* place to keep previous 4-bit value */ + int bufferstep; /* toggle between outputbuffer/output */ + + List outp = new List(); + short[] inp = indata; + var len = indata.Length; + valpred = state.Valprev; + index = state.Index; + step = stepsizeTable[index]; + + bufferstep = 1; + + int k = 0; + for (int i = 0; len > 0; len--, i++) + { + val = inp[i]; + + /* Step 1 - compute difference with previous value */ + diff = val - valpred; + sign = (diff < 0) ? 8 : 0; + if (sign != 0) diff = (-diff); + + /* Step 2 - Divide and clamp */ + /* Note: + ** This code *approximately* computes: + ** delta = diff*4/step; + ** vpdiff = (delta+0.5)*step/4; + ** but in shift step bits are dropped. The net result of this is + ** that even if you have fast mul/div hardware you cannot put it to + ** good use since the fixup would be too expensive. + */ + delta = 0; + vpdiff = (step >> 3); + + if (diff >= step) + { + delta = 4; + diff -= step; + vpdiff += step; + } + step >>= 1; + if (diff >= step) + { + delta |= 2; + diff -= step; + vpdiff += step; + } + step >>= 1; + if (diff >= step) + { + delta |= 1; + vpdiff += step; + } + + /* Step 3 - Update previous value */ + if (sign != 0) + valpred -= vpdiff; + else + valpred += vpdiff; + + /* Step 4 - Clamp previous value to 16 bits */ + if (valpred > 32767) + valpred = 32767; + else if (valpred < -32768) + valpred = -32768; + + /* Step 5 - Assemble value, update index and step values */ + delta |= sign; + + index += indexTable[delta]; + if (index < 0) index = 0; + if (index > 88) index = 88; + step = stepsizeTable[index]; + + /* Step 6 - Output value */ + if (bufferstep != 0) + { + outputbuffer = (delta << 4) & 0xf0; + } + else + { + outp.Add((byte)((delta & 0x0f) | outputbuffer)); + } + bufferstep = bufferstep == 0 ? 1 : 0; + } + + /* Output last step, if needed */ + if (bufferstep == 0) + outp.Add((byte)outputbuffer); + + state.Valprev = (short)valpred; + state.Index = (byte)index; + return outp.ToArray(); + } + + /// + /// 将adpcm转为pcm + /// + /// + /// + /// + public byte[] ToPcm(byte[] data, State state) + { + // signed char *inp; /* Input buffer pointer */ + // short *outp; /* output buffer pointer */ + int sign; /* Current adpcm sign bit */ + int delta; /* Current adpcm output value */ + int step; /* Stepsize */ + int valpred = state.Valprev; /* Predicted value */ + int vpdiff; /* Current change to valpred */ + byte index = state.Index; /* Current step change index */ + int inputbuffer = 0; /* place to keep next 4-bit value */ + bool bufferstep = false; /* toggle between inputbuffer/input */ + + step = stepsizeTable[index]; + + var outdata = new List(); + var len = data.Length * 2; + for (int i = 0; len > 0; len--) + { + /* Step 1 - get the delta value */ + if (bufferstep) + { + delta = inputbuffer & 0xf; + } + else + { + inputbuffer = data[i++]; + delta = (inputbuffer >> 4) & 0xf; + } + bufferstep = !bufferstep; + + /* Step 2 - Find new index value (for later) */ + index += (byte)indexTable[delta]; + if (index < 0) index = 0; + if (index > 88) index = 88; + + /* Step 3 - Separate sign and magnitude */ + sign = delta & 8; + delta &= 7; + + /* Step 4 - Compute difference and new predicted value */ + /* + ** Computes 'vpdiff = (delta+0.5)*step/4', but see comment + ** in adpcm_coder. + */ + vpdiff = step >> 3; + if ((delta & 4) > 0) vpdiff += step; + if ((delta & 2) > 0) vpdiff += step >> 1; + if ((delta & 1) > 0) vpdiff += step >> 2; + + if (sign != 0) + valpred -= vpdiff; + else + valpred += vpdiff; + + /* Step 5 - clamp output value */ + if (valpred > 32767) + valpred = 32767; + else if (valpred < -32768) + valpred = -32768; + + /* Step 6 - Update step value */ + step = stepsizeTable[index]; + + /* Step 7 - Output value */ + outdata.AddRange(BitConverter.GetBytes((short)valpred)); + } + state.Valprev = (short)valpred; + state.Index = index; + return outdata.ToArray(); + } + } + + public static class AdpcmDecoderExtension + { + /// + /// 添加wav头 + /// 仅用于测试pcm是否转成成功,因此没考虑性能,因为播放器可播——# + /// + /// pcm数据 + /// 采样率 + /// 位深 + /// + public static byte[] ToWav(this byte[] input, uint frequency, byte bitDepth = 16) + { + byte[] output = new byte[input.Length + 44]; + Array.Copy(Encoding.ASCII.GetBytes("RIFF"), 0, output, 0, 4); + WriteUint(4, (uint)output.Length - 8, output); + Array.Copy(Encoding.ASCII.GetBytes("WAVE"), 0, output, 8, 4); + Array.Copy(Encoding.ASCII.GetBytes("fmt "), 0, output, 12, 4); + WriteUint(16, 16, output); //Header size + output[20] = 1; //PCM + output[22] = 1; //1 channel + WriteUint(24, frequency, output); //Sample Rate + WriteUint(28, (uint)(frequency * (bitDepth / 8)), output); //Bytes per second + output[32] = (byte)(bitDepth >> 3); //Bytes per sample + output[34] = bitDepth; //Bits per sample + Array.Copy(Encoding.ASCII.GetBytes("data"), 0, output, 36, 4); + WriteUint(40, (uint)output.Length, output); //Date size + Array.Copy(input, 0, output, 44, input.Length); + return output; + } + + private static void WriteUint(uint offset, uint value, byte[] destination) + { + for (int i = 0; i < 4; i++) + { + destination[offset + i] = (byte)(value & 0xFF); + value >>= 8; + } + } + } +} diff --git a/src/JT1078.Flv/Audio/FaacEncoder.cs b/src/JT1078.Flv/Audio/FaacEncoder.cs new file mode 100644 index 0000000..50a58ef --- /dev/null +++ b/src/JT1078.Flv/Audio/FaacEncoder.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using System.Runtime.InteropServices; +using JT1078.Flv.Extensions; + +namespace JT1078.Flv.Audio +{ + public class FaacEncoder : IDisposable + { + private IntPtr faacEncHandle = IntPtr.Zero; + private readonly int inputSamples; + private readonly int maxOutput; + public readonly int frameSize; + private List frameCache = new List(); + public FaacEncoder(int sampleRate, int channels, int sampleBit) + { + var inputSampleBytes = new byte[4]; + var maxOutputBytes = new byte[4]; + + faacEncHandle = FaacEncOpen(sampleRate, channels, inputSampleBytes, maxOutputBytes); + inputSamples = BitConverter.ToInt32(inputSampleBytes, 0); + maxOutput = BitConverter.ToInt32(maxOutputBytes, 0); + frameSize = inputSamples * channels * sampleBit / 8; + + var ptr = FaacEncGetCurrentConfiguration(faacEncHandle); + var configuration = InteropExtensions.IntPtrToStruct(ptr); + configuration.inputFormat = 1; + configuration.outputFormat = 0; + configuration.useTns = 0; + configuration.useLfe = 0; + configuration.aacObjectType = 2; + configuration.shortctl = 0; + configuration.quantqual = 100; + configuration.bandWidth = 0; + configuration.bitRate = 0; + InteropExtensions.IntPtrSetValue(ptr, configuration); + + if (FaacEncSetConfiguration(faacEncHandle, ptr) < 0) throw new Exception("set faac configuration failed!"); + } + + public byte[] Encode(byte[] bytes) + { + frameCache.AddRange(bytes); + if (frameCache.Count() < frameSize)//faac必须达到一帧数据后才能正常编码 + return new byte[0]; + var outputBytes = new byte[maxOutput]; + var len = FaacEncEncode(faacEncHandle, frameCache.Take(frameSize).ToArray(), inputSamples, outputBytes, maxOutput); + frameCache = frameCache.Skip(frameSize).ToList(); + if (len <= 0) + return new byte[0]; + return outputBytes.Take(len).ToArray(); + } + + public void Dispose() + { + if (faacEncHandle != IntPtr.Zero) + { + FaacEncClose(faacEncHandle); + faacEncHandle = IntPtr.Zero; + } + } + + + + const string DLLFile = @"Libs/libfaac.dll"; + + [DllImport(DLLFile, EntryPoint = "faacEncGetVersion", CallingConvention = CallingConvention.StdCall)] + //int FAACAPI faacEncGetVersion(char **faac_id_string, char **faac_copyright_string); + private extern static int FaacEncGetVersion(ref IntPtr faac_id_string, ref IntPtr faac_copyright_string); + + + + [DllImport(DLLFile, EntryPoint = "faacEncGetCurrentConfiguration", CallingConvention = CallingConvention.StdCall)] + //faacEncConfigurationPtr FAACAPI faacEncGetCurrentConfiguration(faacEncHandle hEncoder); + private extern static IntPtr FaacEncGetCurrentConfiguration(IntPtr hEncoder); + + + [DllImport(DLLFile, EntryPoint = "faacEncSetConfiguration", CallingConvention = CallingConvention.StdCall)] + //int FAACAPI faacEncSetConfiguration(faacEncHandle hEncoder,faacEncConfigurationPtr config); + private extern static int FaacEncSetConfiguration(IntPtr hEncoder, IntPtr config); + + [DllImport(DLLFile, EntryPoint = "faacEncOpen", CallingConvention = CallingConvention.StdCall)] + //faacEncHandle FAACAPI faacEncOpen(unsigned long sampleRate, unsigned int numChannels, unsigned long *inputSamples, unsigned long *maxOutputBytes); + private extern static IntPtr FaacEncOpen(int sampleRate, int numChannels, byte[] inputSamples, byte[] maxOutputBytes); + + + [DllImport(DLLFile, EntryPoint = "faacEncGetDecoderSpecificInfo", CallingConvention = CallingConvention.StdCall)] + //int FAACAPI faacEncGetDecoderSpecificInfo(faacEncHandle hEncoder, unsigned char **ppBuffer,unsigned long *pSizeOfDecoderSpecificInfo); + private extern static IntPtr FaacEncGetDecoderSpecificInfo(IntPtr hEncoder, ref IntPtr ppBuffer, ref int pSizeOfDecoderSpecificInfo); + + + [DllImport(DLLFile, EntryPoint = "faacEncEncode", CallingConvention = CallingConvention.StdCall)] + //int FAACAPI faacEncEncode(faacEncHandle hEncoder, int32_t * inputBuffer, unsigned int samplesInput, unsigned char *outputBuffer, unsigned int bufferSize); + private extern static int FaacEncEncode(IntPtr hEncoder, IntPtr inputBuffer, int samplesInput, IntPtr outputBuffer, int bufferSize); + [DllImport(DLLFile, EntryPoint = "faacEncEncode", CallingConvention = CallingConvention.StdCall)] + private extern static int FaacEncEncode(IntPtr hEncoder, byte[] inputBuffer, int samplesInput, byte[] outputBuffer, int bufferSize); + + [DllImport(DLLFile, EntryPoint= "faacEncClose", CallingConvention = CallingConvention.StdCall)] + //int FAACAPI faacEncClose(faacEncHandle hEncoder); + private extern static IntPtr FaacEncClose(IntPtr hEncoder); + + #region 配置结构 + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct FaacEncConfiguration + { + /* config version */ + + public int version; + + /* library version */ + public IntPtr name; + + /* copyright string */ + public IntPtr copyright; + + /* MPEG version, 2 or 4 */ + public uint mpegVersion; + + /* AAC object type + * #define MAIN 1 + #define LOW 2 + #define SSR 3 + #define LTP 4 + * */ + + public uint aacObjectType; + + /* Allow mid/side coding */ + public uint allowMidside; + + /* Use one of the channels as LFE channel */ + public uint useLfe; + + /* Use Temporal Noise Shaping */ + public uint useTns; + + /* bitrate / channel of AAC file */ + public uint bitRate; + + /* AAC file frequency bandwidth */ + public uint bandWidth; + + /* Quantizer quality */ + public uint quantqual; + + /* Bitstream output format (0 = Raw; 1 = ADTS) */ + public int outputFormat; + + /* psychoacoustic model list */ + public IntPtr psymodellist; + + /* selected index in psymodellist */ + public int psymodelidx; + + /* + PCM Sample Input Format + 0 FAAC_INPUT_NULL invalid, signifies a misconfigured config + 1 FAAC_INPUT_16BIT native endian 16bit + 2 FAAC_INPUT_24BIT native endian 24bit in 24 bits (not implemented) + 3 FAAC_INPUT_32BIT native endian 24bit in 32 bits (DEFAULT) + 4 FAAC_INPUT_FLOAT 32bit floating point + */ + public int inputFormat; + + /* block type enforcing (SHORTCTL_NORMAL/SHORTCTL_NOSHORT/SHORTCTL_NOLONG) */ + // #define FAAC_INPUT_NULL 0 + //#define FAAC_INPUT_16BIT 1 + //#define FAAC_INPUT_24BIT 2 + //#define FAAC_INPUT_32BIT 3 + //#define FAAC_INPUT_FLOAT 4 + + //#define SHORTCTL_NORMAL 0 + //#define SHORTCTL_NOSHORT 1 + //#define SHORTCTL_NOLONG 2 + public int shortctl; + + /* + Channel Remapping + + Default 0, 1, 2, 3 ... 63 (64 is MAX_CHANNELS in coder.h) + + WAVE 4.0 2, 0, 1, 3 + WAVE 5.0 2, 0, 1, 3, 4 + WAVE 5.1 2, 0, 1, 4, 5, 3 + AIFF 5.1 2, 0, 3, 1, 4, 5 + */ + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.I4, SizeConst = 64)] + public int[] channel_map; + + } + #endregion + } +} diff --git a/src/JT1078.Flv/Audio/G711ACodec.cs b/src/JT1078.Flv/Audio/G711ACodec.cs new file mode 100644 index 0000000..8344c83 --- /dev/null +++ b/src/JT1078.Flv/Audio/G711ACodec.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace JT1078.Flv.Audio +{ + public class G711ACodec + { + private readonly int SIGN_BIT = 0x80; + private readonly int QUANT_MASK = 0xf; + private readonly int SEG_SHIFT = 4; + private readonly int SEG_MASK = 0x70; + private readonly short[] seg_end = { 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF }; + + static short Search(short val, short[] table, short size) + { + for (short i = 0; i < size; i++) + { + if (val <= table[i]) + { + return i; + } + } + return size; + } + + byte LinearToAlaw(short pcm_val) + { + short mask; + short seg; + char aval; + if (pcm_val >= 0) + { + mask = 0xD5; + } + else + { + mask = 0x55; + pcm_val = (short)(-pcm_val - 1); + if (pcm_val < 0) + { + pcm_val = 32767; + } + } + + //Convert the scaled magnitude to segment number. + seg = Search(pcm_val, seg_end, 8); + + //Combine the sign, segment, and quantization bits. + if (seg >= 8) + { + //out of range, return maximum value. + return (byte)(0x7F ^ mask); + } + else + { + aval = (char)(seg << SEG_SHIFT); + if (seg < 2) aval |= (char)((pcm_val >> 4) & QUANT_MASK); + else aval |= (char)((pcm_val >> (seg + 3)) & QUANT_MASK); + return (byte)(aval ^ mask); + } + } + + + short AlawToLinear(byte value) + { + short t; + short seg; + + value ^= 0x55; + + t = (short)((value & QUANT_MASK) << 4); + seg = (short)((value & SEG_MASK) >> SEG_SHIFT); + switch (seg) + { + case 0: + t += 8; + break; + case 1: + t += 0x108; + break; + default: + t += 0x108; + t <<= seg - 1; + break; + } + return (value & SIGN_BIT) != 0 ? t : (short)-t; + } + + /// + /// 转至PCM + /// + /// + /// + public byte[] ToPcm(byte[] g711data) + { + byte[] pcmdata = new byte[g711data.Length * 2]; + for (int i = 0, offset = 0; i < g711data.Length; i++) + { + short value = AlawToLinear(g711data[i]); + pcmdata[offset++] = (byte)((value >> 8) & 0xff); + pcmdata[offset++] = (byte)(value & 0xff); + } + return pcmdata; + } + + /// + /// 转至G711 + /// + /// + /// + public byte[] ToG711(byte[] pcmdata) + { + byte[] g711data = new byte[pcmdata.Length / 2]; + for (int i = 0, k = 0; i < pcmdata.Length; i += 2, k++) + { + short v = (short)((pcmdata[i + 1] << 8) | (pcmdata[i])); + g711data[k] = LinearToAlaw(v); + } + return g711data; + } + } +} diff --git a/src/JT1078.Flv/Audio/G711UCodec.cs b/src/JT1078.Flv/Audio/G711UCodec.cs new file mode 100644 index 0000000..641999c --- /dev/null +++ b/src/JT1078.Flv/Audio/G711UCodec.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace JT1078.Flv.Audio +{ + public class G711UCodec + { + /* 16384 entries per table (16 bit) */ + readonly byte[] linearToUlawTable = new byte[65536]; + + /* 16384 entries per table (8 bit) */ + readonly short[] ulawToLinearTable = new short[256]; + readonly int SIGN_BIT = 0x80; + readonly int QUANT_MASK = 0x0f; + readonly int SEG_SHIFT = 0x04; + readonly int SEG_MASK = 0x70; + readonly int BIAS = 0x84; + readonly int CLIP = 8159; + readonly short[] seg_uend = { 0x3F, 0x7F, 0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF }; + + public G711UCodec() + { + // 初始化ulaw表 + for (int i = 0; i < 256; i++) ulawToLinearTable[i] = Ulaw2linear((byte)i); + // 初始化ulaw2linear表 + for (int i = 0; i < 65535; i++) linearToUlawTable[i] = Linear2ulaw((short)i); + } + + short Ulaw2linear(byte ulawValue) + { + ulawValue = (byte)(~ulawValue); + short t = (short)(((ulawValue & QUANT_MASK) << 3) + BIAS); + t <<= (ulawValue & SEG_MASK) >> SEG_SHIFT; + + return ((ulawValue & SIGN_BIT) > 0 ? (short)(BIAS - t) : (short)(t - BIAS)); + } + + byte Linear2ulaw(short pcmValue) + { + short mask; + short seg; + byte uval; + + pcmValue = (short)(pcmValue >> 2); + if (pcmValue < 0) + { + pcmValue = (short)(-pcmValue); + mask = 0x7f; + } + else + { + mask = 0xff; + } + + if (pcmValue > CLIP) pcmValue = (short)CLIP; + pcmValue += (short)(BIAS >> 2); + + seg = Search(pcmValue, seg_uend, 8); + + if (seg >= 8) + { + return (byte)(0x7f ^ mask); + } + else + { + uval = (byte)((seg << 4) | ((pcmValue >> (seg + 1)) & 0xF)); + return (byte)(uval ^ mask); + } + } + + short Search(short val, short[] table, short size) + { + for (short i = 0; i < size; i++) + { + if (val <= table[i]) return i; + } + return size; + } + + byte[] UlawToPcm16(byte[] samples) + { + var pcmSamples = new byte[samples.Length * 2]; + for (int i = 0, k = 0; i < samples.Length; i++) + { + short s = ulawToLinearTable[samples[i] & 0xff]; + pcmSamples[k++] = (byte)(s & 0xff); + pcmSamples[k++] = (byte)((s >> 8) & 0xff); + } + return pcmSamples; + } + + private byte[] Pcm16ToUlaw(byte[] pcmSamples) + { + short[] dst = new short[pcmSamples.Length / 2]; + byte[] ulawSamples = new byte[pcmSamples.Length / 2]; + for (int i = 0, k = 0; i < pcmSamples.Length;) + { + dst[k++] = (short)((pcmSamples[i++] & 0xff) | ((pcmSamples[i++] & 0xff) << 8)); + } + for (int i = 0, k = 0; i < dst.Length; i++) + { + ulawSamples[k++] = Linear2ulaw(dst[i]); + } + return ulawSamples; + } + + public byte[] ToPcm(byte[] data) => UlawToPcm16(data); + + public byte[] ToG711(byte[] data) => Pcm16ToUlaw(data); + } +} diff --git a/src/JT1078.Flv/Enums/AACPacketType.cs b/src/JT1078.Flv/Enums/AACPacketType.cs new file mode 100644 index 0000000..a4e9cfb --- /dev/null +++ b/src/JT1078.Flv/Enums/AACPacketType.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace JT1078.Flv.Enums +{ + /// + /// Aac tag-body数据包类型 + /// + public enum AACPacketType + { + /// + /// 音频序列配置 + /// + AudioSpecificConfig = 0, + /// + /// 音频帧 + /// + AudioFrame = 1 + } +} diff --git a/src/JT1078.Flv/Enums/AudioFormat.cs b/src/JT1078.Flv/Enums/AudioFormat.cs new file mode 100644 index 0000000..3fe87d0 --- /dev/null +++ b/src/JT1078.Flv/Enums/AudioFormat.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace JT1078.Flv.Enums +{ + /// + /// 音频格式 + /// + public enum AudioFormat + { + /// + /// Linear PCM, platform endian + /// + Pcm_Platform = 0, + /// + /// ADPCM + /// + ADPCM = 1, + /// + /// MP3 + /// + MP3, + /// + /// Linear PCM, little endian + /// + Pcm_Little = 3, + /// + /// 16-kHz mono + /// + Nellymoser_16Khz = 4, + /// + /// 8-kHz mono + /// + Nellymoser_8Khz = 5, + /// + /// Nellymoser + /// + Nellymoser = 6, + /// + /// A-law logarithmic PCM + /// + G711_A_law = 7, + /// + /// mu-law logarithmic PCM + /// + G711_mu_law = 8, + /// + /// AAC + /// + AAC = 10, + /// + /// Speex + /// + Speex = 11, + /// + /// MP3 8-Khz + /// + MP3_8Khz = 14 + } +} diff --git a/src/JT1078.Flv/Enums/ChannelType.cs b/src/JT1078.Flv/Enums/ChannelType.cs new file mode 100644 index 0000000..b7cf0f9 --- /dev/null +++ b/src/JT1078.Flv/Enums/ChannelType.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace JT1078.Flv.Enums +{ + /// + /// 声道类型 + /// + public enum ChannelType + { + /// + /// 单声道 + /// + Mono, + /// + /// 立体声 + /// + Stereo + } +} diff --git a/src/JT1078.Flv/Enums/SampleBit.cs b/src/JT1078.Flv/Enums/SampleBit.cs new file mode 100644 index 0000000..e788781 --- /dev/null +++ b/src/JT1078.Flv/Enums/SampleBit.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace JT1078.Flv.Enums +{ + /// + /// 采样位深 + /// + public enum SampleBit + { + /// + /// 8位 + /// + Bit_8 = 0, + /// + /// 16位 + /// + Bit_16 = 1 + } +} diff --git a/src/JT1078.Flv/Extensions/InteropExtensions.cs b/src/JT1078.Flv/Extensions/InteropExtensions.cs new file mode 100644 index 0000000..72a7e52 --- /dev/null +++ b/src/JT1078.Flv/Extensions/InteropExtensions.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Runtime.InteropServices; + +namespace JT1078.Flv.Extensions +{ + public static class InteropExtensions + { + public static T BytesToStruct(byte[] bytes, int startIndex, int length) + { + T local; + T local2; + if (bytes == null) + { + local2 = default; + local = local2; + } + else if (bytes.Length <= 0) + { + local2 = default; + local = local2; + } + else + { + IntPtr destination = Marshal.AllocHGlobal(length); + try + { + Marshal.Copy(bytes, startIndex, destination, length); + local = (T)Marshal.PtrToStructure(destination, typeof(T)); + } + catch (Exception exception) + { + throw new Exception("Error in BytesToStruct ! " + exception.Message); + } + finally + { + Marshal.FreeHGlobal(destination); + } + } + return local; + } + + public static void IntPtrSetValue(IntPtr intptr, object structObj) + { + IntPtrSetValue(intptr, StructToBytes(structObj)); + } + + public static void IntPtrSetValue(IntPtr intptr, byte[] bytes) + { + Marshal.Copy(bytes, 0, intptr, bytes.Length); + } + + public static T IntPtrToStruct(IntPtr intptr) + { + int index = 0; + return IntPtrToStruct(intptr, index, Marshal.SizeOf(typeof(T))); + } + + public static T IntPtrToStruct(IntPtr intptr, int index, int length) + { + byte[] destination = new byte[length]; + Marshal.Copy(intptr, destination, index, length); + return BytesToStruct(destination, 0, destination.Length); + } + + public static byte[] StructToBytes(object structObj) + { + int size = Marshal.SizeOf(structObj); + return StructToBytes(structObj, size); + } + + public static byte[] StructToBytes(object structObj, int size) + { + byte[] buffer2; + IntPtr ptr = Marshal.AllocHGlobal(size); + try + { + Marshal.StructureToPtr(structObj, ptr, false); + byte[] destination = new byte[size]; + Marshal.Copy(ptr, destination, 0, size); + buffer2 = destination; + } + catch (Exception exception) + { + throw new Exception("Error in StructToBytes ! " + exception.Message); + } + finally + { + Marshal.FreeHGlobal(ptr); + } + return buffer2; + } + } +} diff --git a/src/JT1078.Flv/FlvEncoder.cs b/src/JT1078.Flv/FlvEncoder.cs index 1548f4f..1a69f3a 100644 --- a/src/JT1078.Flv/FlvEncoder.cs +++ b/src/JT1078.Flv/FlvEncoder.cs @@ -27,7 +27,7 @@ namespace JT1078.Flv private static readonly H264Decoder H264Decoder; private static readonly ConcurrentDictionary VideoSPSDict; private static readonly ConcurrentDictionary FlvFrameInfoDict; - internal static readonly ConcurrentDictionary FirstFlvFrameCache; + internal static readonly ConcurrentDictionary FirstFlvFrameCache; private readonly ILogger logger; static FlvEncoder() { @@ -117,11 +117,11 @@ namespace JT1078.Flv } //cache PreviousTagSize FlvFrameInfoDict.TryAdd(key, new FlvFrameInfo - { - PreviousTagSize = firstFlvKeyFrame.PreviousTagSize, - Interval = (uint)(pps.Timestamp - sps.Timestamp), - Timestamp = pps.Timestamp, - } + { + PreviousTagSize = firstFlvKeyFrame.PreviousTagSize, + Interval = (uint)(pps.Timestamp - sps.Timestamp), + Timestamp = pps.Timestamp, + } ); FirstFlvFrameCache.TryAdd(key, (firstFlvKeyFrame.PreviousTagSize, firstFlvKeyFrame.Buffer, false)); VideoSPSDict.TryAdd(key, spsInfo); @@ -203,7 +203,7 @@ namespace JT1078.Flv /// 由于获取的SIM卡可能为000000000000,所以如果有替换JT1078Package.GetKey()的值 /// 默认65535 /// - public byte[] CreateFlvFrame(JT1078Package package,string key=null,int minimumLength = 65535) + public byte[] CreateFlvFrame(JT1078Package package, string key = null, int minimumLength = 65535) { var nalus = H264Decoder.ParseNALU(package); if (nalus == null || nalus.Count <= 0) return default; @@ -215,7 +215,7 @@ namespace JT1078.Flv /// 设备号+通道号(1111111_1) /// 当前接收到的flv数据 /// - public byte[] GetFirstFlvFrame(string key,byte[] currentBufferFlvFrame) + public byte[] GetFirstFlvFrame(string key, byte[] currentBufferFlvFrame) { if (FirstFlvFrameCache.TryGetValue(key, out var firstBuffer)) { @@ -248,7 +248,7 @@ namespace JT1078.Flv { FlvArrayPool.Return(buffer); } - } + } return default; } @@ -428,10 +428,20 @@ namespace JT1078.Flv /// 1078当前时间戳 /// public ulong Timestamp { get; set; } + + /// + /// 1078当前音频时间戳 + /// + public ulong AudioTimestamp { get; set; } /// /// 与flv上一帧相减的时间间隔 /// public uint Interval { get; set; } + + /// + /// 上一帧音频的时间间隔 + /// + public uint AudioInterval { get; set; } /// /// 1078数据类型 /// diff --git a/src/JT1078.Flv/FlvTags.cs b/src/JT1078.Flv/FlvTags.cs index 13376e1..620621c 100644 --- a/src/JT1078.Flv/FlvTags.cs +++ b/src/JT1078.Flv/FlvTags.cs @@ -27,6 +27,10 @@ namespace JT1078.Flv /// public VideoTags VideoTagsData { get; set; } /// + /// 音频数据 + /// + public AudioTags AudioTagsData { get; set; } + /// /// 根据tag类型 /// public Amf3 DataTagsData { get; set; } diff --git a/src/JT1078.Flv/JT1078.Flv.csproj b/src/JT1078.Flv/JT1078.Flv.csproj index 2c5805c..871a9bd 100644 --- a/src/JT1078.Flv/JT1078.Flv.csproj +++ b/src/JT1078.Flv/JT1078.Flv.csproj @@ -44,4 +44,9 @@ + + + Always + + diff --git a/src/JT1078.Flv/JT1078.Flv.xml b/src/JT1078.Flv/JT1078.Flv.xml index 420425a..ad37679 100644 --- a/src/JT1078.Flv/JT1078.Flv.xml +++ b/src/JT1078.Flv/JT1078.Flv.xml @@ -4,11 +4,153 @@ JT1078.Flv + + + 上一个采样数据,当index为0是该值应该为未压缩的原数据 + + + + + 保留数据(未使用) + + + + + 上一个block最后一个index,第一个block的index=0 + + + + + 将adpcm转为pcm + + + + + + + + 添加wav头 + 仅用于测试pcm是否转成成功,因此没考虑性能,因为播放器可播——# + + pcm数据 + 采样率 + 位深 + + + + + 转至PCM + + + + + + + 转至G711 + + + + + + + Aac tag-body数据包类型 + + + + + 音频序列配置 + + + + + 音频帧 + + + + + 音频格式 + + + + + Linear PCM, platform endian + + + + + ADPCM + + + + + MP3 + + + + + Linear PCM, little endian + + + + + 16-kHz mono + + + + + 8-kHz mono + + + + + Nellymoser + + + + + A-law logarithmic PCM + + + + + mu-law logarithmic PCM + + + + + AAC + + + + + Speex + + + + + MP3 8-Khz + + + + + 声道类型 + + + + + 单声道 + + + + + 立体声 + + ‭00010000‬ @@ -34,6 +176,21 @@ 01010000 + + + 采样位深 + + + + + 8位 + + + + + 16位 + + @@ -94,11 +251,21 @@ 1078当前时间戳 + + + 1078当前音频时间戳 + + 与flv上一帧相减的时间间隔 + + + 上一帧音频的时间间隔 + + 1078数据类型 @@ -127,11 +294,21 @@ 根据tag类型 + + + 音频数据 + + 根据tag类型 + + + 元数据 + + AMF3数据类型 @@ -147,6 +324,48 @@ + + + 其实有很多,这里就固定为立体声 + + + + + 音频类型 + 其实有很多,这里就列几个,如有需要再加 + + + + + 采样率 + AAC固定为3 + 0 = 5.5-kHz + 1 = 11-kHz + 2 = 22-kHz + 3 = 44-kHz + + + + + 采样位深 + + + + + 声道 + AAC永远是1 + + + + + 音频格式 + + + + + 元数据 + + diff --git a/src/JT1078.Flv/Libs/libfaac.dll b/src/JT1078.Flv/Libs/libfaac.dll new file mode 100644 index 0000000000000000000000000000000000000000..c9f2e4588ca41667749854ddeafd69df9fbf9262 GIT binary patch literal 71168 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 )JyE-%3eJz#^9s5Hn?9U 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 Data { get; set; } = new List(); + /// + /// 元数据 + /// + public byte[] RawData => Data.ToArray(); + } +} diff --git a/src/JT1078.Flv/Metadata/AudioSpecificConfig.cs b/src/JT1078.Flv/Metadata/AudioSpecificConfig.cs new file mode 100644 index 0000000..4218b39 --- /dev/null +++ b/src/JT1078.Flv/Metadata/AudioSpecificConfig.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace JT1078.Flv.Metadata +{ + public class AudioSpecificConfig + { + public AudioObjectType AudioType { get; set; } = AudioObjectType.AAC_LC; + public SamplingFrequency SamplingFrequencyIndex { get; set; } = SamplingFrequency.Index_8000; + /// + /// 其实有很多,这里就固定为立体声 + /// + public int ChannelConfiguration { get; set; } = 1; + + public byte[] ToArray() + { + var value = Convert.ToInt16($"{Convert.ToString((int)AudioType, 2).PadLeft(5, '0')}{Convert.ToString((int)SamplingFrequencyIndex, 2).PadLeft(4, '0')}{Convert.ToString(ChannelConfiguration, 2).PadLeft(4, '0')}000", 2); + return new byte[] { (byte)(value >> 8), (byte)value, 0x56, 0xe5, 0x00 }; + } + /// + /// 音频类型 + /// 其实有很多,这里就列几个,如有需要再加 + /// + public enum AudioObjectType + { + AAC_MAIN = 1, + AAC_LC = 2, + AAC_SSR = 3, + AAC_LTP = 4, + SBR = 5, + AAC_SCALABLE + } + public enum SamplingFrequency + { + Index_96000 = 0x00, + Index_88200 = 0x01, + Index_64000 = 0x02, + Index_48000 = 0x03, + Index_44100 = 0x04, + Index_32000 = 0x05, + Index_24000 = 0x06, + Index_22050 = 0x07, + Index_16000 = 0x08, + Index_12000 = 0x09, + Index_11025 = 0x0a, + Index_8000 = 0x0b, + Index_7350 = 0x0c, + ESCAPE = 0x0f + } + } +} diff --git a/src/JT1078.Flv/Metadata/AudioTags.cs b/src/JT1078.Flv/Metadata/AudioTags.cs new file mode 100644 index 0000000..e0cbf10 --- /dev/null +++ b/src/JT1078.Flv/Metadata/AudioTags.cs @@ -0,0 +1,53 @@ +using JT1078.Flv.Enums; +using System; +using System.Collections.Generic; +using System.Text; + +namespace JT1078.Flv.Metadata +{ + public class AudioTags + { + public AudioTags(AACPacketType packetType = AACPacketType.AudioSpecificConfig, byte[] aacFrameData = null) + { + AacPacke = new AacPacke(packetType, aacFrameData); + } + /// + /// 采样率 + /// AAC固定为3 + /// 0 = 5.5-kHz + /// 1 = 11-kHz + /// 2 = 22-kHz + /// 3 = 44-kHz + /// + public int SampleRate => 3; + /// + /// 采样位深 + /// + public SampleBit SampleBit { get; set; } = SampleBit.Bit_16; + /// + /// 声道 + /// AAC永远是1 + /// + public ChannelType Channel => ChannelType.Stereo; + /// + /// 音频格式 + /// + public AudioFormat SoundType => AudioFormat.AAC; + + /// + /// 元数据 + /// + private AacPacke AacPacke { get; set; } + + public byte[] ToArray() + { + var value = $"{Convert.ToString((int)SoundType, 2).PadLeft(4, '0')}{Convert.ToString(SampleRate, 2).PadLeft(2, '0')}{(int)SampleBit}{(int)Channel}"; + var data = new List + { + Convert.ToByte(value, 2) + }; + data.AddRange(AacPacke.RawData); + return data.ToArray(); + } + } +}