diff --git a/BurnOutSharp.Compression/ADPCM/ADPCM_DATA.cs b/BurnOutSharp.Compression/ADPCM/ADPCM_DATA.cs new file mode 100644 index 00000000..1cc22a8c --- /dev/null +++ b/BurnOutSharp.Compression/ADPCM/ADPCM_DATA.cs @@ -0,0 +1,12 @@ +namespace BurnOutSharp.Compression.ADPCM +{ + /// + public unsafe struct ADPCM_DATA + { + public uint[] pValues; + public int BitCount; + public int field_8; + public int field_C; + public int field_10; + } +} \ No newline at end of file diff --git a/BurnOutSharp.Compression/ADPCM/Compressor.cs b/BurnOutSharp.Compression/ADPCM/Compressor.cs new file mode 100644 index 00000000..18f18f6f --- /dev/null +++ b/BurnOutSharp.Compression/ADPCM/Compressor.cs @@ -0,0 +1,131 @@ +using static BurnOutSharp.Compression.ADPCM.Constants; +using static BurnOutSharp.Compression.ADPCM.Helper; + +namespace BurnOutSharp.Compression.ADPCM +{ + public unsafe class Compressor + { + /// + /// Compression routine + /// + /// + public int CompressADPCM(void* pvOutBuffer, int cbOutBuffer, void* pvInBuffer, int cbInBuffer, int ChannelCount, int CompressionLevel) + { + TADPCMStream os = new TADPCMStream(pvOutBuffer, cbOutBuffer); // The output stream + TADPCMStream @is = new TADPCMStream(pvInBuffer, cbInBuffer); // The input stream + byte BitShift = (byte)(CompressionLevel - 1); + short[] PredictedSamples = new short[MAX_ADPCM_CHANNEL_COUNT];// Predicted samples for each channel + short[] StepIndexes = new short[MAX_ADPCM_CHANNEL_COUNT]; // Step indexes for each channel + short InputSample = 0; // Input sample for the current channel + int TotalStepSize; + int ChannelIndex; + int AbsDifference; + int Difference; + int MaxBitMask; + int StepSize; + + // First byte in the output stream contains zero. The second one contains the compression level + os.WriteByteSample(0); + if (!os.WriteByteSample(BitShift)) + return 2; + + // Set the initial step index for each channel + PredictedSamples[0] = PredictedSamples[1] = 0; + StepIndexes[0] = StepIndexes[1] = INITIAL_ADPCM_STEP_INDEX; + + // Next, InitialSample value for each channel follows + for (int i = 0; i < ChannelCount; i++) + { + // Get the initial sample from the input stream + if (!@is.ReadWordSample(ref InputSample)) + return os.LengthProcessed(pvOutBuffer); + + // Store the initial sample to our sample array + PredictedSamples[i] = InputSample; + + // Also store the loaded sample to the output stream + if (!os.WriteWordSample(InputSample)) + return os.LengthProcessed(pvOutBuffer); + } + + // Get the initial index + ChannelIndex = ChannelCount - 1; + + // Now keep reading the input data as long as there is something in the input buffer + while (@is.ReadWordSample(ref InputSample)) + { + int EncodedSample = 0; + + // If we have two channels, we need to flip the channel index + ChannelIndex = (ChannelIndex + 1) % ChannelCount; + + // Get the difference from the previous sample. + // If the difference is negative, set the sign bit to the encoded sample + AbsDifference = InputSample - PredictedSamples[ChannelIndex]; + if (AbsDifference < 0) + { + AbsDifference = -AbsDifference; + EncodedSample |= 0x40; + } + + // If the difference is too low (higher that difference treshold), + // write a step index modifier marker + StepSize = StepSizeTable[StepIndexes[ChannelIndex]]; + if (AbsDifference < (StepSize >> CompressionLevel)) + { + if (StepIndexes[ChannelIndex] != 0) + StepIndexes[ChannelIndex]--; + + os.WriteByteSample(0x80); + } + else + { + // If the difference is too high, write marker that + // indicates increase in step size + while (AbsDifference > (StepSize << 1)) + { + if (StepIndexes[ChannelIndex] >= 0x58) + break; + + // Modify the step index + StepIndexes[ChannelIndex] += 8; + if (StepIndexes[ChannelIndex] > 0x58) + StepIndexes[ChannelIndex] = 0x58; + + // Write the "modify step index" marker + StepSize = StepSizeTable[StepIndexes[ChannelIndex]]; + os.WriteByteSample(0x81); + } + + // Get the limit bit value + MaxBitMask = (1 << (BitShift - 1)); + MaxBitMask = (MaxBitMask > 0x20) ? 0x20 : MaxBitMask; + Difference = StepSize >> BitShift; + TotalStepSize = 0; + + for (int BitVal = 0x01; BitVal <= MaxBitMask; BitVal <<= 1) + { + if ((TotalStepSize + StepSize) <= AbsDifference) + { + TotalStepSize += StepSize; + EncodedSample |= BitVal; + } + StepSize >>= 1; + } + + PredictedSamples[ChannelIndex] = (short)UpdatePredictedSample(PredictedSamples[ChannelIndex], + EncodedSample, + Difference + TotalStepSize); + // Write the encoded sample to the output stream + if (!os.WriteByteSample((byte)EncodedSample)) + break; + + // Calculates the step index to use for the next encode + StepIndexes[ChannelIndex] = GetNextStepIndex(StepIndexes[ChannelIndex], (uint)EncodedSample); + } + } + + return os.LengthProcessed(pvOutBuffer); + } + } +} \ No newline at end of file diff --git a/BurnOutSharp.Compression/ADPCM/Constants.cs b/BurnOutSharp.Compression/ADPCM/Constants.cs new file mode 100644 index 00000000..a885558e --- /dev/null +++ b/BurnOutSharp.Compression/ADPCM/Constants.cs @@ -0,0 +1,51 @@ +namespace BurnOutSharp.Compression.ADPCM +{ + /// + public static class Constants + { + public const int MAX_ADPCM_CHANNEL_COUNT = 2; + + public const byte INITIAL_ADPCM_STEP_INDEX = 0x2C; + + #region Tables necessary for decompression + + public static readonly int[] NextStepTable = + { + -1, 0, -1, 4, -1, 2, -1, 6, + -1, 1, -1, 5, -1, 3, -1, 7, + -1, 1, -1, 5, -1, 3, -1, 7, + -1, 2, -1, 4, -1, 6, -1, 8 + }; + + public 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 + }; + + #endregion + + #region ADPCM decompression present in Starcraft I BETA + + public static readonly uint[] adpcm_values_2 = { 0x33, 0x66 }; + public static readonly uint[] adpcm_values_3 = { 0x3A, 0x3A, 0x50, 0x70 }; + public static readonly uint[] adpcm_values_4 = { 0x3A, 0x3A, 0x3A, 0x3A, 0x4D, 0x66, 0x80, 0x9A }; + public static readonly uint[] adpcm_values_6 = + { + 0x3A, 0x3A, 0x3A, 0x3A, 0x3A, 0x3A, 0x3A, 0x3A, 0x3A, 0x3A, 0x3A, 0x3A, 0x3A, 0x3A, 0x3A, 0x3A, + 0x46, 0x53, 0x60, 0x6D, 0x7A, 0x86, 0x93, 0xA0, 0xAD, 0xBA, 0xC6, 0xD3, 0xE0, 0xED, 0xFA, 0x106 + }; + + #endregion + } +} \ No newline at end of file diff --git a/BurnOutSharp.Compression/ADPCM/Decompressor.cs b/BurnOutSharp.Compression/ADPCM/Decompressor.cs new file mode 100644 index 00000000..4775a1a7 --- /dev/null +++ b/BurnOutSharp.Compression/ADPCM/Decompressor.cs @@ -0,0 +1,205 @@ +using static BurnOutSharp.Compression.ADPCM.Constants; +using static BurnOutSharp.Compression.ADPCM.Helper; + +namespace BurnOutSharp.Compression.ADPCM +{ + public unsafe class Decompressor + { + /// + /// Decompression routine + /// + /// + public int DecompressADPCM(void* pvOutBuffer, int cbOutBuffer, void* pvInBuffer, int cbInBuffer, int ChannelCount) + { + TADPCMStream os = new TADPCMStream(pvOutBuffer, cbOutBuffer); // Output stream + TADPCMStream @is = new TADPCMStream(pvInBuffer, cbInBuffer); // Input stream + byte EncodedSample = 0; + byte BitShift = 0; + short[] PredictedSamples = new short[MAX_ADPCM_CHANNEL_COUNT]; // Predicted sample for each channel + short[] StepIndexes = new short[MAX_ADPCM_CHANNEL_COUNT]; // Predicted step index for each channel + int ChannelIndex; // Current channel index + + // Initialize the StepIndex for each channel + PredictedSamples[0] = PredictedSamples[1] = 0; + StepIndexes[0] = StepIndexes[1] = INITIAL_ADPCM_STEP_INDEX; + + // The first byte is always zero, the second one contains bit shift (compression level - 1) + @is.ReadByteSample(ref BitShift); + @is.ReadByteSample(ref BitShift); + + // Next, InitialSample value for each channel follows + for (int i = 0; i < ChannelCount; i++) + { + // Get the initial sample from the input stream + short InitialSample = 0; + + // Attempt to read the initial sample + if (!@is.ReadWordSample(ref InitialSample)) + return os.LengthProcessed(pvOutBuffer); + + // Store the initial sample to our sample array + PredictedSamples[i] = InitialSample; + + // Also store the loaded sample to the output stream + if (!os.WriteWordSample(InitialSample)) + return os.LengthProcessed(pvOutBuffer); + } + + // Get the initial index + ChannelIndex = ChannelCount - 1; + + // Keep reading as long as there is something in the input buffer + while (@is.ReadByteSample(ref EncodedSample)) + { + // If we have two channels, we need to flip the channel index + ChannelIndex = (ChannelIndex + 1) % ChannelCount; + + if (EncodedSample == 0x80) + { + if (StepIndexes[ChannelIndex] != 0) + StepIndexes[ChannelIndex]--; + + if (!os.WriteWordSample(PredictedSamples[ChannelIndex])) + return os.LengthProcessed(pvOutBuffer); + } + else if (EncodedSample == 0x81) + { + // Modify the step index + StepIndexes[ChannelIndex] += 8; + if (StepIndexes[ChannelIndex] > 0x58) + StepIndexes[ChannelIndex] = 0x58; + + // Next pass, keep going on the same channel + ChannelIndex = (ChannelIndex + 1) % ChannelCount; + } + else + { + int StepIndex = StepIndexes[ChannelIndex]; + int StepSize = StepSizeTable[StepIndex]; + + // Encode one sample + PredictedSamples[ChannelIndex] = (short)DecodeSample(PredictedSamples[ChannelIndex], + EncodedSample, + StepSize, + StepSize >> BitShift); + + // Write the decoded sample to the output stream + if (!os.WriteWordSample(PredictedSamples[ChannelIndex])) + break; + + // Calculates the step index to use for the next encode + StepIndexes[ChannelIndex] = GetNextStepIndex(StepIndex, EncodedSample); + } + } + + // Return total bytes written since beginning of the output buffer + return os.LengthProcessed(pvOutBuffer); + } + + /// + /// ADPCM decompression present in Starcraft I BETA + /// + /// + public int DecompressADPCM_SC1B(void* pvOutBuffer, int cbOutBuffer, void* pvInBuffer, int cbInBuffer, int ChannelCount) + { + TADPCMStream os = new TADPCMStream(pvOutBuffer, cbOutBuffer); // Output stream + TADPCMStream @is = new TADPCMStream(pvInBuffer, cbInBuffer); // Input stream + ADPCM_DATA AdpcmData = new ADPCM_DATA(); + int[] LowBitValues = new int[MAX_ADPCM_CHANNEL_COUNT]; + int[] UpperBits = new int[MAX_ADPCM_CHANNEL_COUNT]; + int[] BitMasks = new int[MAX_ADPCM_CHANNEL_COUNT]; + int[] PredictedSamples = new int[MAX_ADPCM_CHANNEL_COUNT]; + int ChannelIndex; + int ChannelIndexMax; + int OutputSample; + byte BitCount = 0; + byte EncodedSample = 0; + short InputValue16 = 0; + int reg_eax; + int Difference; + + // The first byte contains number of bits + if (!@is.ReadByteSample(ref BitCount)) + return os.LengthProcessed(pvOutBuffer); + if (InitAdpcmData(AdpcmData, BitCount) == null) + return os.LengthProcessed(pvOutBuffer); + + //assert(AdpcmData.pValues != NULL); + + // Init bit values + for (int i = 0; i < ChannelCount; i++) + { + byte OneByte = 0; + + if (!@is.ReadByteSample(ref OneByte)) + return os.LengthProcessed(pvOutBuffer); + LowBitValues[i] = OneByte & 0x01; + UpperBits[i] = OneByte >> 1; + } + + // + for (int i = 0; i < ChannelCount; i++) + { + if (!@is.ReadWordSample(ref InputValue16)) + return os.LengthProcessed(pvOutBuffer); + BitMasks[i] = InputValue16 << AdpcmData.BitCount; + } + + // Next, InitialSample value for each channel follows + for (int i = 0; i < ChannelCount; i++) + { + if (!@is.ReadWordSample(ref InputValue16)) + return os.LengthProcessed(pvOutBuffer); + + PredictedSamples[i] = InputValue16; + os.WriteWordSample(InputValue16); + } + + // Get the initial index + ChannelIndexMax = ChannelCount - 1; + ChannelIndex = 0; + + // Keep reading as long as there is something in the input buffer + while (@is.ReadByteSample(ref EncodedSample)) + { + reg_eax = ((PredictedSamples[ChannelIndex] * 3) << 3) - PredictedSamples[ChannelIndex]; + PredictedSamples[ChannelIndex] = ((reg_eax * 10) + 0x80) >> 8; + + Difference = (((EncodedSample >> 1) + 1) * BitMasks[ChannelIndex] + AdpcmData.field_10) >> AdpcmData.BitCount; + + PredictedSamples[ChannelIndex] = UpdatePredictedSample(PredictedSamples[ChannelIndex], EncodedSample, Difference, 0x01); + + BitMasks[ChannelIndex] = (int)((AdpcmData.pValues[EncodedSample >> 1] * BitMasks[ChannelIndex] + 0x80) >> 6); + if (BitMasks[ChannelIndex] < AdpcmData.field_8) + BitMasks[ChannelIndex] = AdpcmData.field_8; + + if (BitMasks[ChannelIndex] > AdpcmData.field_C) + BitMasks[ChannelIndex] = AdpcmData.field_C; + + reg_eax = (cbInBuffer - @is.LengthProcessed(pvInBuffer)) >> ChannelIndexMax; + OutputSample = PredictedSamples[ChannelIndex]; + if (reg_eax < UpperBits[ChannelIndex]) + { + if (LowBitValues[ChannelIndex] != 0) + { + OutputSample += (UpperBits[ChannelIndex] - reg_eax); + if (OutputSample > 32767) + OutputSample = 32767; + } + else + { + OutputSample += (reg_eax - UpperBits[ChannelIndex]); + if (OutputSample < -32768) + OutputSample = -32768; + } + } + + // Write the word sample and swap channel + os.WriteWordSample((short)(OutputSample)); + ChannelIndex = (ChannelIndex + 1) % ChannelCount; + } + + return os.LengthProcessed(pvOutBuffer); + } + } +} \ No newline at end of file diff --git a/BurnOutSharp.Compression/ADPCM/Helper.cs b/BurnOutSharp.Compression/ADPCM/Helper.cs new file mode 100644 index 00000000..eaf3c568 --- /dev/null +++ b/BurnOutSharp.Compression/ADPCM/Helper.cs @@ -0,0 +1,104 @@ +using static BurnOutSharp.Compression.ADPCM.Constants; + +namespace BurnOutSharp.Compression.ADPCM +{ + /// + internal static unsafe class Helper + { + #region Local functions + + public static short GetNextStepIndex(int StepIndex, uint EncodedSample) + { + // Get the next step index + StepIndex = StepIndex + NextStepTable[EncodedSample & 0x1F]; + + // Don't make the step index overflow + if (StepIndex < 0) + StepIndex = 0; + else if (StepIndex > 88) + StepIndex = 88; + + return (short)StepIndex; + } + + public static int UpdatePredictedSample(int PredictedSample, int EncodedSample, int Difference, int BitMask = 0x40) + { + // Is the sign bit set? + if ((EncodedSample & BitMask) != 0) + { + PredictedSample -= Difference; + if (PredictedSample <= -32768) + PredictedSample = -32768; + } + else + { + PredictedSample += Difference; + if (PredictedSample >= 32767) + PredictedSample = 32767; + } + + return PredictedSample; + } + + public static int DecodeSample(int PredictedSample, int EncodedSample, int StepSize, int Difference) + { + if ((EncodedSample & 0x01) != 0) + Difference += (StepSize >> 0); + + if ((EncodedSample & 0x02) != 0) + Difference += (StepSize >> 1); + + if ((EncodedSample & 0x04) != 0) + Difference += (StepSize >> 2); + + if ((EncodedSample & 0x08) != 0) + Difference += (StepSize >> 3); + + if ((EncodedSample & 0x10) != 0) + Difference += (StepSize >> 4); + + if ((EncodedSample & 0x20) != 0) + Difference += (StepSize >> 5); + + return UpdatePredictedSample(PredictedSample, EncodedSample, Difference); + } + + #endregion + + #region ADPCM decompression present in Starcraft I BETA + + public static uint[] InitAdpcmData(ADPCM_DATA pData, byte BitCount) + { + switch (BitCount) + { + case 2: + pData.pValues = adpcm_values_2; + break; + + case 3: + pData.pValues = adpcm_values_3; + break; + + case 4: + pData.pValues = adpcm_values_4; + break; + + default: + pData.pValues = null; + break; + + case 6: + pData.pValues = adpcm_values_6; + break; + } + + pData.BitCount = BitCount; + pData.field_C = 0x20000; + pData.field_8 = 1 << BitCount; + pData.field_10 = (1 << BitCount) / 2; + return pData.pValues; + } + + #endregion + } +} \ No newline at end of file diff --git a/BurnOutSharp.Compression/ADPCM/TADPCMStream.cs b/BurnOutSharp.Compression/ADPCM/TADPCMStream.cs new file mode 100644 index 00000000..cddad9ab --- /dev/null +++ b/BurnOutSharp.Compression/ADPCM/TADPCMStream.cs @@ -0,0 +1,67 @@ +namespace BurnOutSharp.Compression.ADPCM +{ + /// + /// Helper class for writing output ADPCM data + /// + /// + public unsafe class TADPCMStream + { + private byte* pbBufferEnd; + private byte* pbBuffer; + + public TADPCMStream(void* pvBuffer, int cbBuffer) + { + pbBufferEnd = (byte*)pvBuffer + cbBuffer; + pbBuffer = (byte*)pvBuffer; + } + + public bool ReadByteSample(ref byte ByteSample) + { + // Check if there is enough space in the buffer + if (pbBuffer >= pbBufferEnd) + return false; + + ByteSample = *pbBuffer++; + return true; + } + + public bool WriteByteSample(byte ByteSample) + { + // Check if there is enough space in the buffer + if (pbBuffer >= pbBufferEnd) + return false; + + *pbBuffer++ = ByteSample; + return true; + } + + public bool ReadWordSample(ref short OneSample) + { + // Check if we have enough space in the output buffer + if ((int)(pbBufferEnd - pbBuffer) < sizeof(short)) + return false; + + // Write the sample + OneSample = (short)(pbBuffer[0] + ((pbBuffer[1]) << 0x08)); + pbBuffer += sizeof(short); + return true; + } + + public bool WriteWordSample(short OneSample) + { + // Check if we have enough space in the output buffer + if ((int)(pbBufferEnd - pbBuffer) < sizeof(short)) + return false; + + // Write the sample + *pbBuffer++ = (byte)(OneSample & 0xFF); + *pbBuffer++ = (byte)(OneSample >> 0x08); + return true; + } + + public int LengthProcessed(void* pvOutBuffer) + { + return (int)((byte*)pbBuffer - (byte*)pvOutBuffer); + } + } +} \ No newline at end of file