mirror of
https://github.com/ZDoom/gzdoom.git
synced 2024-11-12 23:54:17 +00:00
Decode audio and video separately in InterplayDecoder
This commit is contained in:
parent
fa2cea3e5b
commit
bf933b3904
2 changed files with 557 additions and 449 deletions
|
@ -94,28 +94,97 @@ static const int16_t delta_table[] = {
|
||||||
#define LE_32(x) (LE_16(x) | ((uint32_t)LE_16(x+2) << 16))
|
#define LE_32(x) (LE_16(x) | ((uint32_t)LE_16(x+2) << 16))
|
||||||
#define LE_64(x) (LE_32(x) | ((uint64_t)LE_32(x+4) << 32))
|
#define LE_64(x) (LE_32(x) | ((uint64_t)LE_32(x+4) << 32))
|
||||||
|
|
||||||
static bool StreamCallbackFunc(SoundStream* stream, void* buff, int len, void* userdata)
|
|
||||||
|
bool InterplayDecoder::StreamCallback(SoundStream *stream, void *buff, int len)
|
||||||
{
|
{
|
||||||
InterplayDecoder* pId = (InterplayDecoder*)userdata;
|
for (int i = 0; i < len;)
|
||||||
memcpy(buff, &pId->audio.samples[pId->audio.nRead], len);
|
{
|
||||||
pId->audio.nRead += len / 2;
|
if(audio.nRead < audio.nWrite)
|
||||||
if (pId->audio.nRead >= (int)countof(pId->audio.samples)) pId->audio.nRead = 0;
|
{
|
||||||
|
int todo = std::min(audio.nWrite-audio.nRead, (len-i) / 2);
|
||||||
|
memcpy((char*)buff+i, &audio.samples[audio.nRead], todo*2);
|
||||||
|
audio.nRead += todo;
|
||||||
|
if (audio.nRead == audio.nWrite)
|
||||||
|
audio.nRead = audio.nWrite = 0;
|
||||||
|
i += todo*2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_lock plock(PacketMutex);
|
||||||
|
while (audio.Packets.empty())
|
||||||
|
{
|
||||||
|
if (!bIsPlaying || ProcessNextChunk() >= CHUNK_SHUTDOWN)
|
||||||
|
{
|
||||||
|
bIsPlaying = false;
|
||||||
|
if (i == 0)
|
||||||
|
return false;
|
||||||
|
memset((char*)buff+i, 0, len-i);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AudioPacket pkt = std::move(audio.Packets.front());
|
||||||
|
audio.Packets.pop_front();
|
||||||
|
plock.unlock();
|
||||||
|
|
||||||
|
int nSamples = pkt.nSize;
|
||||||
|
const uint8_t *samplePtr = pkt.pData.get();
|
||||||
|
if (audio.bCompressed)
|
||||||
|
{
|
||||||
|
int predictor[2];
|
||||||
|
|
||||||
|
for (int ch = 0; ch < audio.nChannels; ch++)
|
||||||
|
{
|
||||||
|
predictor[ch] = (int16_t)LE_16(samplePtr);
|
||||||
|
samplePtr += 2;
|
||||||
|
|
||||||
|
audio.samples[audio.nWrite++] = predictor[ch];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool stereo = audio.nChannels == 2;
|
||||||
|
nSamples -= 2*audio.nChannels;
|
||||||
|
nSamples &= ~(int)stereo;
|
||||||
|
|
||||||
|
int ch = 0;
|
||||||
|
for (int j = 0; j < nSamples; ++j)
|
||||||
|
{
|
||||||
|
predictor[ch] += delta_table[*samplePtr++];
|
||||||
|
predictor[ch] = clamp(predictor[ch], -32768, 32767);
|
||||||
|
|
||||||
|
audio.samples[audio.nWrite++] = predictor[ch];
|
||||||
|
|
||||||
|
// toggle channel
|
||||||
|
ch ^= stereo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (audio.nBitDepth == 8)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < nSamples; ++j)
|
||||||
|
audio.samples[audio.nWrite++] = ((*samplePtr++)-128) << 8;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
nSamples /= 2;
|
||||||
|
for (int j = 0; j < nSamples; ++j)
|
||||||
|
{
|
||||||
|
audio.samples[audio.nWrite++] = (int16_t)LE_16(samplePtr);
|
||||||
|
samplePtr += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
InterplayDecoder::InterplayDecoder(bool soundenabled)
|
InterplayDecoder::InterplayDecoder(bool soundenabled)
|
||||||
{
|
{
|
||||||
bIsPlaying = false;
|
bIsPlaying = false;
|
||||||
bAudioStarted = !soundenabled; // This prevents the stream from getting created
|
bAudioStarted = false;
|
||||||
|
bAudioEnabled = soundenabled;
|
||||||
|
|
||||||
nWidth = 0;
|
nWidth = 0;
|
||||||
nHeight = 0;
|
nHeight = 0;
|
||||||
nFrame = 0;
|
nFrame = 0;
|
||||||
|
|
||||||
memset(palette, 0, sizeof(palette));
|
memset(palette, 0, sizeof(palette));
|
||||||
memset(&audio, 0, sizeof(audio));
|
|
||||||
audio.nRead = 18000; // skip the initial silence. This is needed to sync audio and video because OpenAL's lag is a bit on the high side.
|
|
||||||
|
|
||||||
|
|
||||||
nFps = 0.0;
|
nFps = 0.0;
|
||||||
nFrameDuration = 0;
|
nFrameDuration = 0;
|
||||||
|
@ -146,13 +215,280 @@ void InterplayDecoder::SwapFrames()
|
||||||
nCurrentVideoBuffer = t;
|
nCurrentVideoBuffer = t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int InterplayDecoder::ProcessNextChunk()
|
||||||
|
{
|
||||||
|
uint8_t chunkPreamble[CHUNK_PREAMBLE_SIZE];
|
||||||
|
if (fr.Read(chunkPreamble, CHUNK_PREAMBLE_SIZE) != CHUNK_PREAMBLE_SIZE) {
|
||||||
|
Printf(TEXTCOLOR_RED "InterplayDecoder: could not read from file (EOF?)\n");
|
||||||
|
return CHUNK_EOF;
|
||||||
|
}
|
||||||
|
|
||||||
|
int chunkSize = LE_16(&chunkPreamble[0]);
|
||||||
|
int chunkType = LE_16(&chunkPreamble[2]);
|
||||||
|
|
||||||
|
ChunkData.resize(chunkSize);
|
||||||
|
if (fr.Read(ChunkData.data(), chunkSize) != chunkSize) {
|
||||||
|
Printf(TEXTCOLOR_RED "InterplayDecoder: could not read from file (EOF?)\n");
|
||||||
|
return CHUNK_BAD;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint8_t *palPtr = nullptr;
|
||||||
|
const uint8_t *mapPtr = nullptr;
|
||||||
|
const uint8_t *vidPtr = nullptr;
|
||||||
|
int palStart = 0, palCount = 0;
|
||||||
|
int mapSize = 0, vidSize = 0;
|
||||||
|
|
||||||
|
// iterate through individual opcodes
|
||||||
|
const uint8_t *chunkPtr = ChunkData.data();
|
||||||
|
while (chunkSize > 0 && chunkType != CHUNK_BAD)
|
||||||
|
{
|
||||||
|
if (chunkSize < OPCODE_PREAMBLE_SIZE)
|
||||||
|
{
|
||||||
|
Printf(TEXTCOLOR_RED "InterplayDecoder: opcode size too small\n");
|
||||||
|
return CHUNK_BAD;
|
||||||
|
}
|
||||||
|
int opcodeSize = LE_16(chunkPtr);
|
||||||
|
int opcodeType = chunkPtr[2];
|
||||||
|
int opcodeVersion = chunkPtr[3];
|
||||||
|
|
||||||
|
chunkPtr += OPCODE_PREAMBLE_SIZE;
|
||||||
|
chunkSize -= OPCODE_PREAMBLE_SIZE;
|
||||||
|
if (chunkSize < opcodeSize)
|
||||||
|
{
|
||||||
|
Printf(TEXTCOLOR_RED "InterplayDecoder: opcode size too large for chunk\n");
|
||||||
|
return CHUNK_BAD;
|
||||||
|
}
|
||||||
|
chunkSize -= opcodeSize;
|
||||||
|
|
||||||
|
switch (opcodeType)
|
||||||
|
{
|
||||||
|
case OPCODE_END_OF_STREAM:
|
||||||
|
chunkPtr += opcodeSize;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OPCODE_END_OF_CHUNK:
|
||||||
|
chunkPtr += opcodeSize;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OPCODE_CREATE_TIMER:
|
||||||
|
nTimerRate = LE_32(chunkPtr);
|
||||||
|
nTimerDiv = LE_16(chunkPtr+4);
|
||||||
|
chunkPtr += 6;
|
||||||
|
nFrameDuration = ((uint64_t)nTimerRate * nTimerDiv) * 1000;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OPCODE_INIT_AUDIO_BUFFERS:
|
||||||
|
{
|
||||||
|
// Skip 2 bytes
|
||||||
|
uint16_t flags = LE_16(chunkPtr+2);
|
||||||
|
audio.nSampleRate = LE_16(chunkPtr+4);
|
||||||
|
chunkPtr += 6;
|
||||||
|
|
||||||
|
uint32_t nBufferBytes = (opcodeVersion == 0) ? LE_16(chunkPtr) : LE_32(chunkPtr);
|
||||||
|
chunkPtr += (opcodeVersion == 0) ? 2 : 4;
|
||||||
|
|
||||||
|
audio.nChannels = (flags & 0x1) ? 2 : 1;
|
||||||
|
audio.nBitDepth = (flags & 0x2) ? 16 : 8;
|
||||||
|
audio.bCompressed = (opcodeVersion > 0 && (flags & 0x4));
|
||||||
|
audio.samples = std::make_unique<int16_t[]>(nBufferBytes / (audio.nBitDepth/8));
|
||||||
|
audio.nRead = audio.nWrite = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case OPCODE_START_STOP_AUDIO:
|
||||||
|
chunkPtr += opcodeSize;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OPCODE_INIT_VIDEO_BUFFERS:
|
||||||
|
{
|
||||||
|
assert(((opcodeVersion == 0 && opcodeSize >= 4) ||
|
||||||
|
(opcodeVersion == 1 && opcodeSize >= 6) ||
|
||||||
|
(opcodeVersion == 2 && opcodeSize >= 8)) &&
|
||||||
|
opcodeSize <= 8 && ! videoStride);
|
||||||
|
|
||||||
|
nWidth = LE_16(chunkPtr) * 8;
|
||||||
|
nHeight = LE_16(chunkPtr+2) * 8;
|
||||||
|
|
||||||
|
int count, truecolour;
|
||||||
|
if (opcodeVersion > 0)
|
||||||
|
{
|
||||||
|
count = LE_16(chunkPtr+4);
|
||||||
|
if (opcodeVersion > 1)
|
||||||
|
{
|
||||||
|
truecolour = LE_16(chunkPtr+6);
|
||||||
|
assert(truecolour == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
chunkPtr += opcodeSize;
|
||||||
|
|
||||||
|
pVideoBuffers[0] = new uint8_t[nWidth * nHeight];
|
||||||
|
pVideoBuffers[1] = new uint8_t[nWidth * nHeight];
|
||||||
|
|
||||||
|
videoStride = nWidth;
|
||||||
|
|
||||||
|
animtex.SetSize(AnimTexture::Paletted, nWidth, nHeight);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case OPCODE_UNKNOWN_06:
|
||||||
|
case OPCODE_UNKNOWN_0E:
|
||||||
|
case OPCODE_UNKNOWN_10:
|
||||||
|
case OPCODE_UNKNOWN_12:
|
||||||
|
case OPCODE_UNKNOWN_13:
|
||||||
|
case OPCODE_UNKNOWN_14:
|
||||||
|
case OPCODE_UNKNOWN_15:
|
||||||
|
chunkPtr += opcodeSize;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OPCODE_SEND_BUFFER:
|
||||||
|
//int nPalStart = LE_16(chunkPtr);
|
||||||
|
//int nPalCount = LE_16(chunkPtr+2);
|
||||||
|
|
||||||
|
{
|
||||||
|
VideoPacket pkt;
|
||||||
|
pkt.pData = std::make_unique<uint8_t[]>(palCount*3 + mapSize + vidSize);
|
||||||
|
pkt.nPalStart = palStart;
|
||||||
|
pkt.nPalCount = palCount;
|
||||||
|
pkt.nDecodeMapSize = mapSize;
|
||||||
|
pkt.nVideoDataSize = vidSize;
|
||||||
|
|
||||||
|
if (palPtr)
|
||||||
|
memcpy(pkt.pData.get(), palPtr, palCount*3);
|
||||||
|
if (mapPtr)
|
||||||
|
memcpy(pkt.pData.get() + palCount*3, mapPtr, mapSize);
|
||||||
|
if (vidPtr)
|
||||||
|
memcpy(pkt.pData.get() + palCount*3 + mapSize, vidPtr, vidSize);
|
||||||
|
pkt.bSendFlag = true;
|
||||||
|
VideoPackets.emplace_back(std::move(pkt));
|
||||||
|
}
|
||||||
|
|
||||||
|
palPtr = nullptr;
|
||||||
|
palStart = palCount = 0;
|
||||||
|
mapPtr = nullptr;
|
||||||
|
mapSize = 0;
|
||||||
|
vidPtr = nullptr;
|
||||||
|
vidSize = 0;
|
||||||
|
|
||||||
|
chunkPtr += opcodeSize;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OPCODE_AUDIO_FRAME:
|
||||||
|
{
|
||||||
|
uint16_t seqIndex = LE_16(chunkPtr);
|
||||||
|
uint16_t streamMask = LE_16(chunkPtr+2);
|
||||||
|
uint16_t nSamples = LE_16(chunkPtr+4); // number of samples this chunk(?)
|
||||||
|
chunkPtr += 6;
|
||||||
|
|
||||||
|
// We only bother with stream 0
|
||||||
|
if (!(streamMask & 1) || !bAudioEnabled)
|
||||||
|
{
|
||||||
|
chunkPtr += opcodeSize - 6;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioPacket pkt;
|
||||||
|
pkt.nSize = opcodeSize - 6;
|
||||||
|
pkt.pData = std::make_unique<uint8_t[]>(pkt.nSize);
|
||||||
|
memcpy(pkt.pData.get(), chunkPtr, pkt.nSize);
|
||||||
|
audio.Packets.emplace_back(std::move(pkt));
|
||||||
|
|
||||||
|
chunkPtr += opcodeSize - 6;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case OPCODE_SILENCE_FRAME:
|
||||||
|
chunkPtr += opcodeSize;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OPCODE_INIT_VIDEO_MODE:
|
||||||
|
chunkPtr += opcodeSize;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OPCODE_CREATE_GRADIENT:
|
||||||
|
chunkPtr += opcodeSize;
|
||||||
|
Printf("InterplayDecoder: Create gradient not supported.\n");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OPCODE_SET_PALETTE:
|
||||||
|
if (opcodeSize > 0x304 || opcodeSize < 4) {
|
||||||
|
Printf("set_palette opcode with invalid size\n");
|
||||||
|
chunkType = CHUNK_BAD;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
palStart = LE_16(chunkPtr);
|
||||||
|
palCount = LE_16(chunkPtr+2);
|
||||||
|
palPtr = chunkPtr + 4;
|
||||||
|
if (palStart > 255 || palStart+palCount > 256) {
|
||||||
|
Printf("set_palette indices out of range (%d -> %d)\n", palStart, palStart+palCount-1);
|
||||||
|
chunkType = CHUNK_BAD;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (opcodeSize-4 < palCount*3) {
|
||||||
|
Printf("set_palette opcode too small (%d < %d)\n", opcodeSize-4, palCount*3);
|
||||||
|
chunkType = CHUNK_BAD;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkPtr += opcodeSize;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OPCODE_SET_PALETTE_COMPRESSED:
|
||||||
|
chunkPtr += opcodeSize;
|
||||||
|
Printf("InterplayDecoder: Set palette compressed not supported.\n");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OPCODE_SET_DECODING_MAP:
|
||||||
|
mapPtr = chunkPtr;
|
||||||
|
mapSize = opcodeSize;
|
||||||
|
|
||||||
|
chunkPtr += opcodeSize;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OPCODE_VIDEO_DATA:
|
||||||
|
vidPtr = chunkPtr;
|
||||||
|
vidSize = opcodeSize;
|
||||||
|
|
||||||
|
chunkPtr += opcodeSize;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
Printf("InterplayDecoder: Unknown opcode (0x%x v%d, %d bytes).\n", opcodeType, opcodeVersion, opcodeSize);
|
||||||
|
chunkPtr += opcodeSize;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunkType < CHUNK_SHUTDOWN && (palPtr || mapPtr || vidPtr))
|
||||||
|
{
|
||||||
|
VideoPacket pkt;
|
||||||
|
pkt.pData = std::make_unique<uint8_t[]>(palCount*3 + mapSize + vidSize);
|
||||||
|
pkt.nPalStart = palStart;
|
||||||
|
pkt.nPalCount = palCount;
|
||||||
|
pkt.nDecodeMapSize = mapSize;
|
||||||
|
pkt.nVideoDataSize = vidSize;
|
||||||
|
|
||||||
|
if (palPtr)
|
||||||
|
memcpy(pkt.pData.get(), palPtr, palCount*3);
|
||||||
|
if(mapPtr)
|
||||||
|
memcpy(pkt.pData.get() + palCount*3, mapPtr, mapSize);
|
||||||
|
if(vidPtr)
|
||||||
|
memcpy(pkt.pData.get() + palCount*3 + mapSize, vidPtr, vidSize);
|
||||||
|
pkt.bSendFlag = false;
|
||||||
|
VideoPackets.emplace_back(std::move(pkt));
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunkType;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void InterplayDecoder::Close()
|
void InterplayDecoder::Close()
|
||||||
{
|
{
|
||||||
fr.Close();
|
|
||||||
bIsPlaying = false;
|
bIsPlaying = false;
|
||||||
if (stream)
|
if (stream)
|
||||||
S_StopCustomStream(stream);
|
S_StopCustomStream(stream);
|
||||||
stream = nullptr;
|
stream = nullptr;
|
||||||
|
fr.Close();
|
||||||
|
|
||||||
if (decodeMap.pData) {
|
if (decodeMap.pData) {
|
||||||
delete[] decodeMap.pData;
|
delete[] decodeMap.pData;
|
||||||
|
@ -187,367 +523,114 @@ bool InterplayDecoder::Open(FileReader &fr_)
|
||||||
fr_.Seek(6, FileReader::SeekCur);
|
fr_.Seek(6, FileReader::SeekCur);
|
||||||
fr = std::move(fr_);
|
fr = std::move(fr_);
|
||||||
|
|
||||||
//Run();
|
if (ProcessNextChunk() != CHUNK_INIT_VIDEO)
|
||||||
|
{
|
||||||
|
Printf(TEXTCOLOR_RED "InterplayDecoder: First chunk not CHUNK_INIT_VIDEO\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t chunkPreamble[CHUNK_PREAMBLE_SIZE];
|
||||||
|
if (fr.Read(chunkPreamble, CHUNK_PREAMBLE_SIZE) != CHUNK_PREAMBLE_SIZE) {
|
||||||
|
Printf(TEXTCOLOR_RED "InterplayDecoder: could not read from file (EOF?)\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
fr.Seek(-CHUNK_PREAMBLE_SIZE, FileReader::SeekCur);
|
||||||
|
|
||||||
|
int chunkType = LE_16(&chunkPreamble[2]);
|
||||||
|
if (chunkType == CHUNK_VIDEO)
|
||||||
|
bAudioEnabled = false;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (ProcessNextChunk() != CHUNK_INIT_AUDIO)
|
||||||
|
{
|
||||||
|
Printf(TEXTCOLOR_RED "InterplayDecoder: Second non-video chunk not CHUNK_INIT_AUDIO\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bAudioEnabled = audio.nSampleRate > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bIsPlaying = true;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool InterplayDecoder::RunFrame(uint64_t clock)
|
bool InterplayDecoder::RunFrame(uint64_t clock)
|
||||||
{
|
{
|
||||||
uint8_t chunkPreamble[CHUNK_PREAMBLE_SIZE];
|
|
||||||
uint8_t opcodePreamble[OPCODE_PREAMBLE_SIZE];
|
|
||||||
uint8_t opcodeType;
|
|
||||||
uint8_t opcodeVersion;
|
|
||||||
int opcodeSize, chunkSize;
|
|
||||||
int chunkType = 0;
|
|
||||||
|
|
||||||
// iterate through the chunks in the file
|
|
||||||
do
|
|
||||||
{
|
|
||||||
// handle timing - wait until we're ready to process the next frame.
|
// handle timing - wait until we're ready to process the next frame.
|
||||||
if (nNextFrameTime > clock) {
|
if (nNextFrameTime > clock) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
nNextFrameTime += nFrameDuration;
|
nNextFrameTime += nFrameDuration;
|
||||||
}
|
|
||||||
|
|
||||||
if (fr.Read(chunkPreamble, CHUNK_PREAMBLE_SIZE) != CHUNK_PREAMBLE_SIZE) {
|
if (!bAudioStarted && bAudioEnabled)
|
||||||
Printf(TEXTCOLOR_RED "InterplayDecoder: could not read from file (EOF?)\n");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
chunkSize = LE_16(&chunkPreamble[0]);
|
|
||||||
chunkType = LE_16(&chunkPreamble[2]);
|
|
||||||
|
|
||||||
ChunkData.resize(chunkSize);
|
|
||||||
if (fr.Read(ChunkData.data(), chunkSize) != chunkSize) {
|
|
||||||
Printf(TEXTCOLOR_RED "InterplayDecoder: could not read from file (EOF?)\n");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// iterate through individual opcodes
|
|
||||||
ChunkPtr = ChunkData.data();
|
|
||||||
while (chunkSize > 0 && chunkType != CHUNK_BAD)
|
|
||||||
{
|
|
||||||
if (chunkSize < OPCODE_PREAMBLE_SIZE)
|
|
||||||
{
|
|
||||||
Printf(TEXTCOLOR_RED "InterplayDecoder: opcode size too small\n");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
opcodeSize = LE_16(ChunkPtr);
|
|
||||||
opcodeType = ChunkPtr[2];
|
|
||||||
opcodeVersion = ChunkPtr[3];
|
|
||||||
|
|
||||||
ChunkPtr += OPCODE_PREAMBLE_SIZE;
|
|
||||||
chunkSize -= OPCODE_PREAMBLE_SIZE;
|
|
||||||
if (chunkSize < opcodeSize)
|
|
||||||
{
|
|
||||||
Printf(TEXTCOLOR_RED "InterplayDecoder: opcode size too large for chunk\n");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
chunkSize -= opcodeSize;
|
|
||||||
|
|
||||||
switch (opcodeType)
|
|
||||||
{
|
|
||||||
case OPCODE_END_OF_STREAM:
|
|
||||||
{
|
|
||||||
ChunkPtr += opcodeSize;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case OPCODE_END_OF_CHUNK:
|
|
||||||
{
|
|
||||||
ChunkPtr += opcodeSize;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case OPCODE_CREATE_TIMER:
|
|
||||||
{
|
|
||||||
nTimerRate = LE_32(ChunkPtr);
|
|
||||||
nTimerDiv = LE_16(ChunkPtr+4);
|
|
||||||
ChunkPtr += 6;
|
|
||||||
nFrameDuration = ((uint64_t)nTimerRate * nTimerDiv) * 1000;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case OPCODE_INIT_AUDIO_BUFFERS:
|
|
||||||
{
|
|
||||||
// Skip 2 bytes
|
|
||||||
uint16_t flags = LE_16(ChunkPtr+2);
|
|
||||||
audio.nSampleRate = LE_16(ChunkPtr+4);
|
|
||||||
ChunkPtr += 6;
|
|
||||||
|
|
||||||
uint32_t nBufferBytes = (opcodeVersion == 0) ? LE_16(ChunkPtr) : LE_32(ChunkPtr);
|
|
||||||
ChunkPtr += (opcodeVersion == 0) ? 2 : 4;
|
|
||||||
|
|
||||||
audio.nChannels = (flags & 0x1) ? 2 : 1;
|
|
||||||
audio.nBitDepth = (flags & 0x2) ? 16 : 8;
|
|
||||||
audio.bCompressed = (opcodeVersion > 0 && (flags & 0x4));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case OPCODE_START_STOP_AUDIO:
|
|
||||||
{
|
|
||||||
if (!bAudioStarted)
|
|
||||||
{
|
{
|
||||||
S_StopMusic(true);
|
S_StopMusic(true);
|
||||||
// start audio playback
|
// start audio playback
|
||||||
stream = S_CreateCustomStream(6000, audio.nSampleRate, audio.nChannels, MusicSamples16bit, StreamCallbackFunc, this);
|
stream = S_CreateCustomStream(6000, audio.nSampleRate, audio.nChannels, MusicSamples16bit, StreamCallbackC, this);
|
||||||
bAudioStarted = true;
|
bAudioStarted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChunkPtr += opcodeSize;
|
bool doFrame = false;
|
||||||
break;
|
do
|
||||||
}
|
|
||||||
|
|
||||||
case OPCODE_INIT_VIDEO_BUFFERS:
|
|
||||||
{
|
{
|
||||||
assert(((opcodeVersion == 0 && opcodeSize >= 4) ||
|
std::unique_lock plock(PacketMutex);
|
||||||
(opcodeVersion == 1 && opcodeSize >= 6) ||
|
while (VideoPackets.empty())
|
||||||
(opcodeVersion == 2 && opcodeSize >= 8)) &&
|
|
||||||
opcodeSize <= 8);
|
|
||||||
|
|
||||||
nWidth = LE_16(ChunkPtr) * 8;
|
|
||||||
nHeight = LE_16(ChunkPtr+2) * 8;
|
|
||||||
|
|
||||||
int count, truecolour;
|
|
||||||
if (opcodeVersion > 0)
|
|
||||||
{
|
{
|
||||||
count = LE_16(ChunkPtr+4);
|
if (!bIsPlaying || ProcessNextChunk() >= CHUNK_SHUTDOWN)
|
||||||
if (opcodeVersion > 1)
|
|
||||||
{
|
{
|
||||||
truecolour = LE_16(ChunkPtr+6);
|
bIsPlaying = false;
|
||||||
assert(truecolour == 0);
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChunkPtr += opcodeSize;
|
VideoPacket pkt = std::move(VideoPackets.front());
|
||||||
|
VideoPackets.pop_front();
|
||||||
|
plock.unlock();
|
||||||
|
|
||||||
pVideoBuffers[0] = new uint8_t[nWidth * nHeight];
|
const uint8_t *palData = pkt.pData.get();
|
||||||
pVideoBuffers[1] = new uint8_t[nWidth * nHeight];
|
const uint8_t *mapData = palData + pkt.nPalCount*3;
|
||||||
|
const uint8_t *vidData = mapData + pkt.nDecodeMapSize;
|
||||||
|
|
||||||
videoStride = nWidth;
|
if (pkt.nPalCount > 0)
|
||||||
|
|
||||||
animtex.SetSize(AnimTexture::Paletted, nWidth, nHeight);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case OPCODE_UNKNOWN_06:
|
|
||||||
case OPCODE_UNKNOWN_0E:
|
|
||||||
case OPCODE_UNKNOWN_10:
|
|
||||||
case OPCODE_UNKNOWN_12:
|
|
||||||
case OPCODE_UNKNOWN_13:
|
|
||||||
case OPCODE_UNKNOWN_14:
|
|
||||||
case OPCODE_UNKNOWN_15:
|
|
||||||
{
|
{
|
||||||
ChunkPtr += opcodeSize;
|
int nPalEnd = pkt.nPalStart + pkt.nPalCount;
|
||||||
break;
|
for (int i = pkt.nPalStart; i < nPalEnd; i++)
|
||||||
}
|
|
||||||
|
|
||||||
case OPCODE_SEND_BUFFER:
|
|
||||||
{
|
{
|
||||||
int nPalStart = LE_16(ChunkPtr);
|
palette[i].r = (*palData++) << 2;
|
||||||
int nPalCount = LE_16(ChunkPtr+2);
|
palette[i].g = (*palData++) << 2;
|
||||||
ChunkPtr += opcodeSize;
|
palette[i].b = (*palData++) << 2;
|
||||||
|
|
||||||
animtex.SetFrame(&palette[0].r , GetCurrentFrame());
|
|
||||||
|
|
||||||
nFrame++;
|
|
||||||
SwapFrames();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case OPCODE_AUDIO_FRAME:
|
|
||||||
{
|
|
||||||
auto pStart = ChunkPtr;
|
|
||||||
uint16_t seqIndex = LE_16(ChunkPtr);
|
|
||||||
uint16_t streamMask = LE_16(ChunkPtr+2);
|
|
||||||
uint16_t nSamples = LE_16(ChunkPtr+4); // number of samples this chunk(?)
|
|
||||||
ChunkPtr += 6;
|
|
||||||
|
|
||||||
// We only bother with stream 0
|
|
||||||
if (!(streamMask & 1))
|
|
||||||
{
|
|
||||||
ChunkPtr += opcodeSize - 6;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
nSamples = opcodeSize - 6;
|
|
||||||
if (audio.bCompressed)
|
|
||||||
{
|
|
||||||
int predictor[2];
|
|
||||||
|
|
||||||
for (int ch = 0; ch < audio.nChannels; ch++)
|
|
||||||
{
|
|
||||||
predictor[ch] = (int16_t)LE_16(ChunkPtr);
|
|
||||||
ChunkPtr += 2;
|
|
||||||
|
|
||||||
audio.samples[audio.nWrite++] = predictor[ch];
|
|
||||||
if (audio.nWrite >= (int)countof(audio.samples)) audio.nWrite = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool stereo = audio.nChannels == 2;
|
|
||||||
nSamples -= 2*audio.nChannels;
|
|
||||||
nSamples &= ~(int)stereo;
|
|
||||||
|
|
||||||
int ch = 0;
|
|
||||||
for (int i = 0; i < nSamples;)
|
|
||||||
{
|
|
||||||
int todo = std::min(nSamples-i, (int)std::size(audio.samples)-audio.nWrite);
|
|
||||||
auto end = ChunkPtr + todo;
|
|
||||||
while(ChunkPtr != end)
|
|
||||||
{
|
|
||||||
predictor[ch] += delta_table[*ChunkPtr++];
|
|
||||||
predictor[ch] = clamp(predictor[ch], -32768, 32767);
|
|
||||||
|
|
||||||
audio.samples[audio.nWrite++] = predictor[ch];
|
|
||||||
|
|
||||||
// toggle channel
|
|
||||||
ch ^= stereo;
|
|
||||||
}
|
|
||||||
if (audio.nWrite >= (int)countof(audio.samples)) audio.nWrite = 0;
|
|
||||||
i += todo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (audio.nBitDepth == 8)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < nSamples;)
|
|
||||||
{
|
|
||||||
int todo = std::min(nSamples-i, (int)std::size(audio.samples)-audio.nWrite);
|
|
||||||
auto end = ChunkPtr + todo;
|
|
||||||
while(ChunkPtr != end)
|
|
||||||
audio.samples[audio.nWrite++] = ((*ChunkPtr++)-128) << 8;
|
|
||||||
if (audio.nWrite >= (int)countof(audio.samples)) audio.nWrite = 0;
|
|
||||||
i += todo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
nSamples /= 2;
|
|
||||||
for (int i = 0; i < nSamples;)
|
|
||||||
{
|
|
||||||
int todo = std::min(nSamples-i, (int)std::size(audio.samples)-audio.nWrite);
|
|
||||||
auto end = ChunkPtr + todo*2;
|
|
||||||
while(ChunkPtr != end)
|
|
||||||
{
|
|
||||||
audio.samples[audio.nWrite++] = (int16_t)LE_16(ChunkPtr);
|
|
||||||
ChunkPtr += 2;
|
|
||||||
}
|
|
||||||
if (audio.nWrite >= (int)countof(audio.samples)) audio.nWrite = 0;
|
|
||||||
i += todo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto pEnd = ChunkPtr;
|
|
||||||
int nRead = (int)(pEnd - pStart);
|
|
||||||
assert(opcodeSize == nRead);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case OPCODE_SILENCE_FRAME:
|
|
||||||
{
|
|
||||||
uint16_t seqIndex = LE_16(ChunkPtr);
|
|
||||||
uint16_t streamMask = LE_16(ChunkPtr+2);
|
|
||||||
uint16_t nStreamLen = LE_16(ChunkPtr+4);
|
|
||||||
ChunkPtr += 6;
|
|
||||||
|
|
||||||
if (streamMask & 1)
|
|
||||||
{
|
|
||||||
nStreamLen = (opcodeSize-6) * 8 / audio.nBitDepth;
|
|
||||||
|
|
||||||
for (int i = 0; i < nStreamLen;)
|
|
||||||
{
|
|
||||||
int todo = std::min(nStreamLen-i, (int)std::size(audio.samples)-audio.nWrite);
|
|
||||||
memset(&audio.samples[audio.nWrite], 0, todo*2);
|
|
||||||
audio.nWrite += todo;
|
|
||||||
if (audio.nWrite >= (int)countof(audio.samples)) audio.nWrite = 0;
|
|
||||||
i += todo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case OPCODE_INIT_VIDEO_MODE:
|
|
||||||
{
|
|
||||||
ChunkPtr += opcodeSize;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case OPCODE_CREATE_GRADIENT:
|
|
||||||
{
|
|
||||||
ChunkPtr += opcodeSize;
|
|
||||||
Printf("InterplayDecoder: Create gradient not supported.\n");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case OPCODE_SET_PALETTE:
|
|
||||||
{
|
|
||||||
if (opcodeSize > 0x304 || opcodeSize < 4) {
|
|
||||||
Printf("set_palette opcode with invalid size\n");
|
|
||||||
chunkType = CHUNK_BAD;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
int nPalStart = LE_16(ChunkPtr);
|
|
||||||
int nPalEnd = nPalStart + LE_16(ChunkPtr+2) - 1;
|
|
||||||
ChunkPtr += 4;
|
|
||||||
if (nPalStart > 255 || nPalEnd > 255) {
|
|
||||||
Printf("set_palette indices out of range (%d -> %d)\n", nPalStart, nPalEnd);
|
|
||||||
chunkType = CHUNK_BAD;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
for (int i = nPalStart; i <= nPalEnd; i++)
|
|
||||||
{
|
|
||||||
palette[i].r = (*ChunkPtr++) << 2;
|
|
||||||
palette[i].g = (*ChunkPtr++) << 2;
|
|
||||||
palette[i].b = (*ChunkPtr++) << 2;
|
|
||||||
palette[i].r |= palette[i].r >> 6;
|
palette[i].r |= palette[i].r >> 6;
|
||||||
palette[i].g |= palette[i].g >> 6;
|
palette[i].g |= palette[i].g >> 6;
|
||||||
palette[i].b |= palette[i].b >> 6;
|
palette[i].b |= palette[i].b >> 6;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case OPCODE_SET_PALETTE_COMPRESSED:
|
if (pkt.nDecodeMapSize > 0)
|
||||||
{
|
|
||||||
ChunkPtr += opcodeSize;
|
|
||||||
Printf("InterplayDecoder: Set palette compressed not supported.\n");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case OPCODE_SET_DECODING_MAP:
|
|
||||||
{
|
{
|
||||||
if (!decodeMap.pData)
|
if (!decodeMap.pData)
|
||||||
{
|
{
|
||||||
decodeMap.pData = new uint8_t[opcodeSize];
|
decodeMap.pData = new uint8_t[pkt.nDecodeMapSize];
|
||||||
decodeMap.nSize = opcodeSize;
|
decodeMap.nSize = pkt.nDecodeMapSize;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (opcodeSize != (int)decodeMap.nSize) {
|
if (pkt.nDecodeMapSize != decodeMap.nSize) {
|
||||||
delete[] decodeMap.pData;
|
delete[] decodeMap.pData;
|
||||||
decodeMap.pData = new uint8_t[opcodeSize];
|
decodeMap.pData = new uint8_t[pkt.nDecodeMapSize];
|
||||||
decodeMap.nSize = opcodeSize;
|
decodeMap.nSize = pkt.nDecodeMapSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
memcpy(decodeMap.pData, ChunkPtr, opcodeSize);
|
memcpy(decodeMap.pData, mapData, pkt.nDecodeMapSize);
|
||||||
ChunkPtr += opcodeSize;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case OPCODE_VIDEO_DATA:
|
if (pkt.nVideoDataSize > 0 && decodeMap.nSize > 0)
|
||||||
{
|
{
|
||||||
auto pStart = ChunkPtr;
|
auto pStart = vidData;
|
||||||
|
|
||||||
// need to skip 14 bytes
|
// need to skip 14 bytes
|
||||||
ChunkPtr += 14;
|
ChunkPtr = pStart + 14;
|
||||||
|
|
||||||
if (decodeMap.nSize)
|
|
||||||
{
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
for (uint32_t y = 0; y < nHeight; y += 8)
|
for (uint32_t y = 0; y < nHeight; y += 8)
|
||||||
{
|
{
|
||||||
for (uint32_t x = 0; x < nWidth; x += 8)
|
for (uint32_t x = 0; x < nWidth; x += 8)
|
||||||
|
@ -617,24 +700,22 @@ bool InterplayDecoder::RunFrame(uint64_t clock)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
auto pEnd = ChunkPtr;
|
auto pEnd = ChunkPtr;
|
||||||
int nSkipBytes = opcodeSize - (int)(pEnd - pStart); // we can end up with 1 byte left we need to skip
|
// we can end up with 1 byte left we need to skip
|
||||||
|
int nSkipBytes = pkt.nVideoDataSize - (int)(pEnd - pStart);
|
||||||
assert(nSkipBytes <= 1);
|
assert(nSkipBytes <= 1);
|
||||||
|
|
||||||
ChunkPtr += nSkipBytes;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
doFrame = pkt.bSendFlag;
|
||||||
break;
|
} while(!doFrame);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
animtex.SetFrame(&palette[0].r , GetCurrentFrame());
|
||||||
while (chunkType < CHUNK_VIDEO && bIsPlaying);
|
|
||||||
return chunkType != CHUNK_END;
|
nFrame++;
|
||||||
|
SwapFrames();
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InterplayDecoder::CopyBlock(uint8_t* pDest, uint8_t* pSrc)
|
void InterplayDecoder::CopyBlock(uint8_t* pDest, uint8_t* pSrc)
|
||||||
|
|
|
@ -44,6 +44,9 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "files.h"
|
#include "files.h"
|
||||||
|
@ -55,6 +58,21 @@
|
||||||
|
|
||||||
class InterplayDecoder
|
class InterplayDecoder
|
||||||
{
|
{
|
||||||
|
struct AudioPacket
|
||||||
|
{
|
||||||
|
size_t nSize = 0;
|
||||||
|
std::unique_ptr<uint8_t[]> pData;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VideoPacket
|
||||||
|
{
|
||||||
|
uint16_t nPalStart=0, nPalCount=0;
|
||||||
|
uint32_t nDecodeMapSize = 0;
|
||||||
|
uint32_t nVideoDataSize = 0;
|
||||||
|
bool bSendFlag = false;
|
||||||
|
std::unique_ptr<uint8_t[]> pData;
|
||||||
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum
|
enum
|
||||||
{
|
{
|
||||||
|
@ -109,15 +127,16 @@ public:
|
||||||
|
|
||||||
struct AudioData
|
struct AudioData
|
||||||
{
|
{
|
||||||
int hFx;
|
int nChannels = 0;
|
||||||
int nChannels;
|
uint16_t nSampleRate = 0;
|
||||||
uint16_t nSampleRate;
|
uint8_t nBitDepth = 0;
|
||||||
uint8_t nBitDepth;
|
bool bCompressed = false;
|
||||||
bool bCompressed;
|
|
||||||
|
|
||||||
int16_t samples[6000 * kAudioBlocks]; // must be a multiple of the stream buffer size
|
std::unique_ptr<int16_t[]> samples;
|
||||||
int nWrite;
|
int nWrite = 0;
|
||||||
int nRead;
|
int nRead = 0;
|
||||||
|
|
||||||
|
std::deque<AudioPacket> Packets;
|
||||||
};
|
};
|
||||||
|
|
||||||
AudioData audio;
|
AudioData audio;
|
||||||
|
@ -126,6 +145,10 @@ public:
|
||||||
AnimTextures& animTex() { return animtex; }
|
AnimTextures& animTex() { return animtex; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool StreamCallback(SoundStream *stream, void *buff, int len);
|
||||||
|
static bool StreamCallbackC(SoundStream *stream, void *buff, int len, void *userdata)
|
||||||
|
{ return static_cast<InterplayDecoder*>(userdata)->StreamCallback(stream, buff, len); }
|
||||||
|
|
||||||
struct DecodeMap
|
struct DecodeMap
|
||||||
{
|
{
|
||||||
uint8_t* pData;
|
uint8_t* pData;
|
||||||
|
@ -159,9 +182,11 @@ private:
|
||||||
void DecodeBlock14(int32_t offset);
|
void DecodeBlock14(int32_t offset);
|
||||||
void DecodeBlock15(int32_t offset);
|
void DecodeBlock15(int32_t offset);
|
||||||
|
|
||||||
|
std::mutex PacketMutex;
|
||||||
FileReader fr;
|
FileReader fr;
|
||||||
|
|
||||||
bool bIsPlaying, bAudioStarted;
|
bool bIsPlaying, bAudioStarted;
|
||||||
|
bool bAudioEnabled;
|
||||||
|
|
||||||
uint32_t nTimerRate, nTimerDiv;
|
uint32_t nTimerRate, nTimerDiv;
|
||||||
uint32_t nWidth, nHeight, nFrame;
|
uint32_t nWidth, nHeight, nFrame;
|
||||||
|
@ -169,12 +194,14 @@ private:
|
||||||
uint64_t nFrameDuration;
|
uint64_t nFrameDuration;
|
||||||
|
|
||||||
std::vector<uint8_t> ChunkData;
|
std::vector<uint8_t> ChunkData;
|
||||||
const uint8_t *ChunkPtr = nullptr;
|
int ProcessNextChunk();
|
||||||
|
|
||||||
|
std::deque<VideoPacket> VideoPackets;
|
||||||
uint8_t* pVideoBuffers[2];
|
uint8_t* pVideoBuffers[2];
|
||||||
uint32_t nCurrentVideoBuffer, nPreviousVideoBuffer;
|
uint32_t nCurrentVideoBuffer, nPreviousVideoBuffer;
|
||||||
int32_t videoStride;
|
int32_t videoStride;
|
||||||
|
|
||||||
|
const uint8_t *ChunkPtr = nullptr;
|
||||||
DecodeMap decodeMap;
|
DecodeMap decodeMap;
|
||||||
|
|
||||||
Palette palette[256];
|
Palette palette[256];
|
||||||
|
|
Loading…
Reference in a new issue