From 35fbbc2f31a30f7880c1e9d7cfbb1a597812852a Mon Sep 17 00:00:00 2001 From: "giantpune@gmail.com" Date: Thu, 6 Jan 2011 11:56:02 +0000 Subject: [PATCH] * add a thp player. based off dimok & thakis' work ( requires libjpeg ) --- .gitattributes | 20 + thp_player/ffw.png | Bin 0 -> 5379 bytes thp_player/gcvid.cpp | 890 ++++++++++++++++++++++++++++++++++++++ thp_player/gcvid.h | 341 +++++++++++++++ thp_player/main.cpp | 12 + thp_player/next.png | Bin 0 -> 5389 bytes thp_player/pause.png | Bin 0 -> 4949 bytes thp_player/play.png | Bin 0 -> 5014 bytes thp_player/prev.png | Bin 0 -> 5329 bytes thp_player/rc.qrc | 16 + thp_player/repeat.png | Bin 0 -> 5629 bytes thp_player/rev.png | Bin 0 -> 5332 bytes thp_player/star.png | Bin 0 -> 5449 bytes thp_player/stop.png | Bin 0 -> 5003 bytes thp_player/thp_player.pro | 26 ++ thp_player/thpwindow.cpp | 376 ++++++++++++++++ thp_player/thpwindow.h | 73 ++++ thp_player/thpwindow.ui | 485 +++++++++++++++++++++ thp_player/vol_high.png | Bin 0 -> 5682 bytes thp_player/vol_low.png | Bin 0 -> 5165 bytes thp_player/vol_med.png | Bin 0 -> 5375 bytes 21 files changed, 2239 insertions(+) create mode 100644 thp_player/ffw.png create mode 100755 thp_player/gcvid.cpp create mode 100755 thp_player/gcvid.h create mode 100644 thp_player/main.cpp create mode 100644 thp_player/next.png create mode 100644 thp_player/pause.png create mode 100644 thp_player/play.png create mode 100644 thp_player/prev.png create mode 100644 thp_player/rc.qrc create mode 100644 thp_player/repeat.png create mode 100644 thp_player/rev.png create mode 100644 thp_player/star.png create mode 100644 thp_player/stop.png create mode 100644 thp_player/thp_player.pro create mode 100644 thp_player/thpwindow.cpp create mode 100644 thp_player/thpwindow.h create mode 100644 thp_player/thpwindow.ui create mode 100644 thp_player/vol_high.png create mode 100644 thp_player/vol_low.png create mode 100644 thp_player/vol_med.png diff --git a/.gitattributes b/.gitattributes index 7a3cea3..6171986 100644 --- a/.gitattributes +++ b/.gitattributes @@ -131,3 +131,23 @@ saveToy/saveloadthread.h -text saveToy/textdialog.cpp -text saveToy/textdialog.h -text saveToy/textdialog.ui -text +thp_player/ffw.png -text +thp_player/gcvid.cpp -text +thp_player/gcvid.h -text +thp_player/main.cpp -text +thp_player/next.png -text +thp_player/pause.png -text +thp_player/play.png -text +thp_player/prev.png -text +thp_player/rc.qrc -text +thp_player/repeat.png -text +thp_player/rev.png -text +thp_player/star.png -text +thp_player/stop.png -text +thp_player/thp_player.pro -text +thp_player/thpwindow.cpp -text +thp_player/thpwindow.h -text +thp_player/thpwindow.ui -text +thp_player/vol_high.png -text +thp_player/vol_low.png -text +thp_player/vol_med.png -text diff --git a/thp_player/ffw.png b/thp_player/ffw.png new file mode 100644 index 0000000000000000000000000000000000000000..0954fb5b4bfe83dbf17e9498bd01ced04b6c61bf GIT binary patch literal 5379 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 //NULL +#include //memcmp +#include +#include +#include +#include +using namespace std; + +void readThpHeader(FILE* f, ThpHeader& h) +{ + fread(&h, sizeof(h), 1, f); + h.version = qFromBigEndian(h.version); + h.maxBufferSize = qFromBigEndian(h.maxBufferSize); + h.maxAudioSamples = qFromBigEndian(h.maxAudioSamples); + u32 * ptr = (u32 *) &h.fps; + *ptr = qFromBigEndian(*ptr); + h.numFrames = qFromBigEndian(h.numFrames); + h.firstFrameSize = qFromBigEndian(h.firstFrameSize); + h.dataSize = qFromBigEndian(h.dataSize); + h.componentDataOffset = qFromBigEndian(h.componentDataOffset); + h.offsetsDataOffset = qFromBigEndian(h.offsetsDataOffset); + h.firstFrameOffset = qFromBigEndian(h.firstFrameOffset); + h.lastFrameOffset = qFromBigEndian(h.lastFrameOffset); +} + +void readThpComponents(FILE* f, ThpComponents& c) +{ + fread(&c, sizeof(c), 1, f); + c.numComponents = qFromBigEndian(c.numComponents); +} + +void readThpVideoInfo(FILE* f, ThpVideoInfo& i, bool isVersion11) +{ + fread(&i, sizeof(i), 1, f); + i.width = qFromBigEndian(i.width); + i.height = qFromBigEndian(i.height); + if(isVersion11) + i.unknown = qFromBigEndian(i.unknown); + else + { + i.unknown = 0; + fseek(f, -4, SEEK_CUR); + } +} + +void readThpAudioInfo(FILE* f, ThpAudioInfo& i, bool isVersion11) +{ + fread(&i, sizeof(i), 1, f); + i.numChannels = qFromBigEndian(i.numChannels); + i.frequency = qFromBigEndian(i.frequency); + i.numSamples = qFromBigEndian(i.numSamples); + if(isVersion11) + i.numData = qFromBigEndian(i.numData); + else + { + i.numData = 1; + fseek(f, -4, SEEK_CUR); + } +} + +void readMthHeader(FILE* f, MthHeader& h) +{ + fread(&h, sizeof(h), 1, f); + + h.unknown = qFromBigEndian(h.unknown); + h.unknown2 = qFromBigEndian(h.unknown2); + h.maxFrameSize = qFromBigEndian(h.maxFrameSize); + h.width = qFromBigEndian(h.width); + h.height = qFromBigEndian(h.height); + h.fps = qFromBigEndian(h.fps); + h.numFrames = qFromBigEndian(h.numFrames); + h.offset = qFromBigEndian(h.offset); + h.unknown5 = qFromBigEndian(h.unknown5); + h.firstFrameSize = qFromBigEndian(h.firstFrameSize); +} + +struct DecStruct +{ + const u8* currSrcByte; + u32 blockCount; + u8 index; + u8 shift; +}; + +void thpAudioInitialize(DecStruct& s, const u8* srcStart) +{ + s.currSrcByte = srcStart; + s.blockCount = 2; + s.index = (*s.currSrcByte >> 4) & 0x7; + s.shift = *s.currSrcByte & 0xf; + ++s.currSrcByte; +} + +s32 thpAudioGetNewSample(DecStruct& s) +{ + //the following if is executed all 14 calls + //to thpAudioGetNewSample() (once for each + //microblock) because mask & 0xf can contain + //16 different values and starts with 2 + if((s.blockCount & 0xf) == 0) + { + s.index = (*s.currSrcByte >> 4) & 0x7; + s.shift = *s.currSrcByte & 0xf; + ++s.currSrcByte; + s.blockCount += 2; + } + + s32 ret; + if((s.blockCount & 1) != 0) + { + s32 t = (*s.currSrcByte << 28) & 0xf0000000; + ret = t >> 28; //this has to be an arithmetic shift + ++s.currSrcByte; + } + else + { + s32 t = (*s.currSrcByte << 24) & 0xf0000000; + ret = t >> 28; //this has to be an arithmetic shift + } + + ++s.blockCount; + return ret; +} + +int thpAudioDecode(s16 * destBuffer, const u8* srcBuffer, bool separateChannelsInOutput, bool isInputStereo) +{ + if(destBuffer == NULL || srcBuffer == NULL) + return 0; + + ThpAudioBlockHeader* head = (ThpAudioBlockHeader*)srcBuffer; + + u32 channelInSize = qFromBigEndian(head->channelSize); + u32 numSamples = qFromBigEndian(head->numSamples); + + const u8* srcChannel1 = srcBuffer + sizeof(ThpAudioBlockHeader); + const u8* srcChannel2 = srcChannel1 + channelInSize; + + s16* table1 = head->table1; + s16* table2 = head->table2; + + s16* destChannel1, * destChannel2; + u32 delta; + + if(separateChannelsInOutput) + { + //separated channels in output + destChannel1 = destBuffer; + destChannel2 = destBuffer + numSamples; + delta = 1; + } + else + { + //interleaved channels in output + destChannel1 = destBuffer; + destChannel2 = destBuffer + 1; + delta = 2; + } + + DecStruct s; + if(!isInputStereo) + { + //mono channel in input + + thpAudioInitialize(s, srcChannel1); + + s16 prev1 = qFromBigEndian(*(s16*)(srcBuffer + 72)); + s16 prev2 = qFromBigEndian(*(s16*)(srcBuffer + 74)); + + for(u32 i = 0; i < numSamples; ++i) + { + s64 res = (s64)thpAudioGetNewSample(s); + res = ((res << s.shift) << 11); //convert to 53.11 fixedpoint + + //these values are 53.11 fixed point numbers + s64 val1 = qFromBigEndian(table1[2*s.index]); + s64 val2 = qFromBigEndian(table1[2*s.index + 1]); + + //convert to 48.16 fixed point + res = (val1*prev1 + val2*prev2 + res) << 5; + + //rounding: + u16 decimalPlaces = res & 0xffff; + if(decimalPlaces > 0x8000) //i.e. > 0.5 + //round up + ++res; + else if(decimalPlaces == 0x8000) //i.e. == 0.5 + if((res & 0x10000) != 0) + //round up every other number + ++res; + + //get nonfractional parts of number, clamp to [-32768, 32767] + s32 final = (res >> 16); + if(final > 32767) final = 32767; + else if(final < -32768) final = -32768; + + prev2 = prev1; + prev1 = final; + *destChannel1 = (s16)final; + *destChannel2 = (s16)final; + destChannel1 += delta; + destChannel2 += delta; + } + } + else + { + //two channels in input - nearly the same as for one channel, + //so no comments here (different lines are marked with XXX) + + thpAudioInitialize(s, srcChannel1); + s16 prev1 = qFromBigEndian(*(s16*)(srcBuffer + 72)); + s16 prev2 = qFromBigEndian(*(s16*)(srcBuffer + 74)); + for(u32 i = 0; i < numSamples; ++i) + { + s64 res = (s64)thpAudioGetNewSample(s); + res = ((res << s.shift) << 11); + s64 val1 = qFromBigEndian(table1[2*s.index]); + s64 val2 = qFromBigEndian(table1[2*s.index + 1]); + res = (val1*prev1 + val2*prev2 + res) << 5; + u16 decimalPlaces = res & 0xffff; + if(decimalPlaces > 0x8000) + ++res; + else if(decimalPlaces == 0x8000) + if((res & 0x10000) != 0) + ++res; + s32 final = (res >> 16); + if(final > 32767) final = 32767; + else if(final < -32768) final = -32768; + prev2 = prev1; + prev1 = final; + *destChannel1 = (s16)final; + destChannel1 += delta; + } + + thpAudioInitialize(s, srcChannel2);//XXX + prev1 = qFromBigEndian(*(s16*)(srcBuffer + 76));//XXX + prev2 = qFromBigEndian(*(s16*)(srcBuffer + 78));//XXX + for(u32 j = 0; j < numSamples; ++j) + { + s64 res = (s64)thpAudioGetNewSample(s); + res = ((res << s.shift) << 11); + s64 val1 = qFromBigEndian(table2[2*s.index]);//XXX + s64 val2 = qFromBigEndian(table2[2*s.index + 1]);//XXX + res = (val1*prev1 + val2*prev2 + res) << 5; + u16 decimalPlaces = res & 0xffff; + if(decimalPlaces > 0x8000) + ++res; + else if(decimalPlaces == 0x8000) + if((res & 0x10000) != 0) + ++res; + s32 final = (res >> 16); + if(final > 32767) final = 32767; + else if(final < -32768) final = -32768; + prev2 = prev1; + prev1 = final; + *destChannel2 = (s16)final; + destChannel2 += delta; + } + } + + return numSamples; +} + + +VideoFrame::VideoFrame() + : _data(NULL), _w(0), _h(0), _p(0) +{} + +VideoFrame::~VideoFrame() +{ dealloc(); } + +void VideoFrame::resize(int width, int height) +{ + if(width == _w && height == _h) + return; + + dealloc(); + _w = width; + _h = height; + + //24 bpp, 4 byte padding + _p = 3*width; + _p += (4 - _p%4)%4; + + _data = new u8[_p*_h]; +} + +int VideoFrame::getWidth() const +{ return _w; } + +int VideoFrame::getHeight() const +{ return _h; } + +int VideoFrame::getPitch() const +{ return _p; } + +u8* VideoFrame::getData() +{ return _data; } + +const u8* VideoFrame::getData() const +{ return _data; } + +void VideoFrame::dealloc() +{ + if(_data != NULL) + delete [] _data; + _data = NULL; + _w = _h = _p = 0; +} + +//swaps red and blue channel of a video frame +void swapRB(VideoFrame& f) +{ + u8* currLine = f.getData(); + + int hyt = f.getHeight(); + int pitch = f.getPitch(); + + for(int y = 0; y < hyt; ++y) + { + for(int x = 0, x2 = 2; x < pitch; x += 3, x2 += 3) + { + u8 t = currLine[x]; + currLine[x] = currLine[x2]; + currLine[x2] = t; + } + currLine += pitch; + } +} + +enum FILETYPE +{ + THP, MTH, JPG, + UNKNOWN = -1 +}; + +FILETYPE getFiletype(FILE* f) +{ + long t = ftell(f); + fseek(f, 0, SEEK_SET); + + u8 buff[4]; + fread(buff, 1, 4, f); + + FILETYPE ret = UNKNOWN; + if(memcmp("THP\0", buff, 4) == 0) + ret = THP; + else if(memcmp("MTHP", buff, 4) == 0) + ret = MTH; + else if(buff[0] == 0xff && buff[1] == 0xd8) + ret = JPG; + + fseek(f, t, SEEK_SET); + return ret; +} + +long getFilesize(FILE* f) +{ + long t = ftell(f); + fseek(f, 0, SEEK_END); + long ret = ftell(f); + fseek(f, t, SEEK_SET); + return ret; +} + +void decodeJpeg(const u8* data, int size, VideoFrame& dest); + + +VideoFile::VideoFile(FILE* f) + : _f(f) +{} + +VideoFile::~VideoFile() +{ + if(_f != NULL) + fclose(_f); + _f = NULL; +} + +int VideoFile::getWidth() const +{ return 0; } + +int VideoFile::getHeight() const +{ return 0; } + +float VideoFile::getFps() const +{ return 0.f; } + +int VideoFile::getFrameCount() const +{ return 0; } + +int VideoFile::getCurrentFrameNr() const +{ return 0; } + +void VideoFile::loadNextFrame() +{} +void VideoFile::SetFrameNo( u32 frameNo ){ Q_UNUSED( frameNo ); } + +void VideoFile::getCurrentFrame(VideoFrame& f) const +{ +Q_UNUSED( f ); } + +bool VideoFile::hasSound() const +{ return false; } + +int VideoFile::getNumChannels() const +{ return 0; } + +int VideoFile::getFrequency() const +{ return 0; } + +int VideoFile::getMaxAudioSamples() const +{ return 0; } + +int VideoFile::getCurrentBuffer(s16* data) const +{ + Q_UNUSED( data ); + return 0; } + +void VideoFile::loadFrame(VideoFrame& frame, const u8* data, int size) const +{ + decodeJpeg(data, size, frame); +} + + +ThpVideoFile::ThpVideoFile(FILE* f) + : VideoFile(f) +{ + readThpHeader(f, _head); + + //this is just to find files that have this field != 0, i + //have no such a file + assert(_head.offsetsDataOffset == 0); + + readThpComponents(f, _components); + for(u32 i = 0; i < _components.numComponents; ++i) + { + if(_components.componentTypes[i] == 0) //video + readThpVideoInfo(_f, _videoInfo, _head.version == 0x00011000); + else if(_components.componentTypes[i] == 1) //audio + { + readThpAudioInfo(_f, _audioInfo, _head.version == 0x00011000); + assert(_head.maxAudioSamples != 0); + } + } + + _numInts = 3; + if(_head.maxAudioSamples != 0) + _numInts = 4; + + _currFrameNr = -1; + _nextFrameOffset = _head.firstFrameOffset; + _nextFrameSize = _head.firstFrameSize; + _currFrameData.resize(_head.maxBufferSize); //include some padding + loadNextFrame(); +} + +int ThpVideoFile::getWidth() const +{ return _videoInfo.width; } + +int ThpVideoFile::getHeight() const +{ return _videoInfo.height; } + +float ThpVideoFile::getFps() const +{ return _head.fps; } + +int ThpVideoFile::getFrameCount() const +{ return _head.numFrames; } + +int ThpVideoFile::getCurrentFrameNr() const +{ return _currFrameNr; } + +//TODO, really seek to correct frame +void ThpVideoFile::SetFrameNo( u32 frameNo ) +{ + Q_UNUSED( frameNo ); + _currFrameNr = 0; + _nextFrameOffset = _head.firstFrameOffset; + _nextFrameSize = _head.firstFrameSize; +} + +void ThpVideoFile::loadNextFrame() +{ + ++_currFrameNr; + if(_currFrameNr >= (int) _head.numFrames) + { + _currFrameNr = 0; + _nextFrameOffset = _head.firstFrameOffset; + _nextFrameSize = _head.firstFrameSize; + } + + fseek(_f, _nextFrameOffset, SEEK_SET); + fread(&_currFrameData[0], 1, _nextFrameSize, _f); + + _nextFrameOffset += _nextFrameSize; + _nextFrameSize = qFromBigEndian(*(u32*)&_currFrameData[0]); +} + +void ThpVideoFile::getCurrentFrame(VideoFrame& f) const +{ + int size = qFromBigEndian(*(u32*)(&_currFrameData[0] + 8)); + loadFrame(f, &_currFrameData[0] + 4*_numInts, size); +} + +bool ThpVideoFile::hasSound() const +{ return _head.maxAudioSamples != 0; } + +int ThpVideoFile::getNumChannels() const +{ + if(hasSound()) + return _audioInfo.numChannels; + else + return 0; +} + +int ThpVideoFile::getFrequency() const +{ + if(hasSound()) + return _audioInfo.frequency; + else + return 0; +} + +int ThpVideoFile::getMaxAudioSamples() const +{ return _head.maxAudioSamples; } + +int ThpVideoFile::getCurrentBuffer(s16* data) const +{ + if(!hasSound()) + return 0; + + int jpegSize = qFromBigEndian(*(u32*)(&_currFrameData[0] + 8)); + const u8* src = &_currFrameData[0] + _numInts*4 + jpegSize; + + return thpAudioDecode(data, src, false, _audioInfo.numChannels == 2); +} + +MthVideoFile::MthVideoFile(FILE* f) + : VideoFile(f) +{ + readMthHeader(f, _head); + + _currFrameNr = -1; + _nextFrameOffset = _head.offset; + _nextFrameSize = _head.firstFrameSize; + _thisFrameSize = 0; + + _currFrameData.resize(_head.maxFrameSize); + loadNextFrame(); +} + + +int MthVideoFile::getWidth() const +{ return _head.width; } + +int MthVideoFile::getHeight() const +{ return _head.height; } + +float MthVideoFile::getFps() const +{ + return (float) 1.0f*_head.fps; //TODO: This has to be in there somewhere +} + +int MthVideoFile::getFrameCount() const +{ + return _head.numFrames; +} + +int MthVideoFile::getCurrentFrameNr() const +{ return _currFrameNr; } + +//TODO, really seek to correct frame +void MthVideoFile::SetFrameNo( u32 frameNo ) +{ + Q_UNUSED( frameNo ); + _currFrameNr = 0; + _nextFrameOffset = _head.offset; + _nextFrameSize = _head.firstFrameSize; +} + +void MthVideoFile::loadNextFrame() +{ + ++_currFrameNr; + if(_currFrameNr >= (int) _head.numFrames) + { + _currFrameNr = 0; + _nextFrameOffset = _head.offset; + _nextFrameSize = _head.firstFrameSize; + } + + fseek(_f, _nextFrameOffset, SEEK_SET); + _currFrameData.resize(_nextFrameSize); + fread(&_currFrameData[0], 1, _nextFrameSize, _f); + _thisFrameSize = _nextFrameSize; + + u32 nextSize; + nextSize = qFromBigEndian(*(u32*)(&_currFrameData[0])); + _nextFrameOffset += _nextFrameSize; + _nextFrameSize = nextSize; + +} + +void MthVideoFile::getCurrentFrame(VideoFrame& f) const +{ + int size = _thisFrameSize; + loadFrame(f, &_currFrameData[0] + 4, size - 4); +} + + +JpgVideoFile::JpgVideoFile(FILE* f) + : VideoFile(f) +{ + vector data(getFilesize(f)); + fread(&data[0], 1, getFilesize(f), f); + + loadFrame(_currFrame, &data[0], getFilesize(f)); +} + +int JpgVideoFile::getWidth() const +{ return _currFrame.getWidth(); } + +int JpgVideoFile::getHeight() const +{ return _currFrame.getHeight(); } + +int JpgVideoFile::getFrameCount() const +{ return 1; } + +void JpgVideoFile::getCurrentFrame(VideoFrame& f) const +{ + f.resize(_currFrame.getWidth(), _currFrame.getHeight()); + memcpy(f.getData(), _currFrame.getData(),f.getPitch()*f.getHeight()); +} + +VideoFile* openVideo(const string& fileName) +{ + FILE* f = fopen(fileName.c_str(), "rb"); + if(f == NULL) + return NULL; + + FILETYPE type = getFiletype(f); + switch(type) + { + case THP: + return new ThpVideoFile(f); + case MTH: + return new MthVideoFile(f); + case JPG: + return new JpgVideoFile(f); + + default: + fclose(f); + return NULL; + } +} + +void closeVideo(VideoFile*& vf) +{ + if(vf != NULL) + delete vf; + vf = NULL; +} + +//as mentioned above, we have to convert 0xff to 0xff 0x00 +//after the image date has begun (ie, after the 0xff 0xda marker) +//but we must not convert the end-of-image-marker (0xff 0xd9) +//this way. There may be 0xff 0xd9 bytes embedded in the image +//data though, so I add 4 bytes to the input buffer +//and fill them with zeroes and check for 0xff 0xd9 0 0 +//as end-of-image marker. this is not correct, but works +//and is easier to code... ;-) +//a better solution would be to patch jpeglib so that this conversion +//is not neccessary + +u8 endBytesThp[] = { 0xff, 0xd9, 0, 0 }; //used in thp files +u8 endBytesMth[] = { 0xff, 0xd9, 0xff, 0 }; //used in mth files + +int countRequiredSize(const u8* data, int size, int& start, int& end) +{ + start = 2*size; + int count = 0; + + int j; + for(j = size - 1; data[j] == 0; --j) + ; //search end of data + + if(data[j] == 0xd9) //thp file + end = j - 1; + else if(data[j] == 0xff) //mth file + end = j - 2; + + for(int i = 0; i < end; ++i) + { + if(data[i] == 0xff) + { + //if i == srcSize - 1, then this would normally overrun src - that's why 4 padding + //bytes are included at the end of src + if(data[i + 1] == 0xda && start == 2*size) + start = i; + if(i > start) + ++count; + } + } + return size + count; +} + +void convertToRealJpeg(u8* dest, const u8* src, int srcSize, int start, int end) +{ + int di = 0; + for(int i = 0; i < srcSize; ++i, ++di) + { + dest[di] = src[i]; + //if i == srcSize - 1, then this would normally overrun src - that's why 4 padding + //bytes are included at the end of src + if(src[i] == 0xff && i > start && i < end) + { + ++di; + dest[di] = 0; + } + } +} + +void decodeRealJpeg(const u8* data, int size, VideoFrame& dest); + +void decodeJpeg(const u8* data, int size, VideoFrame& dest) +{ + //convert format so jpeglib understands it... + int start = 0, end = 0; + int newSize = countRequiredSize(data, size, start, end); + u8* buff = new u8[newSize]; + convertToRealJpeg(buff, data, size, start, end); + + //...and feed it to jpeglib + decodeRealJpeg(buff, newSize, dest); + + delete [] buff; +} + +extern "C" +{ +#include +} + +//the following functions are needed to let +//libjpeg read from memory instead of from a file... +//it's a little clumsy to do :-| +const u8* g_jpegBuffer; +int g_jpegSize; +bool g_isLoading = false; + +void jpegInitSource(j_decompress_ptr cinfo) +{ Q_UNUSED( cinfo ); } + +boolean jpegFillInputBuffer(j_decompress_ptr cinfo) +{ + cinfo->src->next_input_byte = g_jpegBuffer; + cinfo->src->bytes_in_buffer = g_jpegSize; + return TRUE; +} + +void jpegSkipInputData(j_decompress_ptr cinfo, long num_bytes) +{ + cinfo->src->next_input_byte += num_bytes; + cinfo->src->bytes_in_buffer -= num_bytes; +} + +boolean jpegResyncToRestart(j_decompress_ptr cinfo, int desired) +{ + jpeg_resync_to_restart(cinfo, desired); + return TRUE; +} + +void jpegTermSource(j_decompress_ptr cinfo) +{ Q_UNUSED( cinfo ); } + +void jpegErrorHandler(j_common_ptr cinfo) +{ + char buff[1024]; + (*cinfo->err->format_message)(cinfo, buff); + //MessageBox(g_hWnd, buff, "JpegLib error:", MB_OK); +} + +void decodeRealJpeg(const u8* data, int size, VideoFrame& dest) +{ + if(g_isLoading) + return; + g_isLoading = true; + + /* + //debug + FILE* fout = fopen("curr.jpg", "wb"); + fwrite(data, size, 1, fout); + fclose(fout); + //*/ + + //decompressor state + jpeg_decompress_struct cinfo; + jpeg_error_mgr errorMgr; + + //read from memory manager + jpeg_source_mgr sourceMgr; + + cinfo.err = jpeg_std_error(&errorMgr); + errorMgr.error_exit = jpegErrorHandler; + + jpeg_create_decompress(&cinfo); + + //setup read-from-memory + g_jpegBuffer = data; + g_jpegSize = size; + sourceMgr.bytes_in_buffer = size; + sourceMgr.next_input_byte = data; + sourceMgr.init_source = jpegInitSource; + sourceMgr.fill_input_buffer = jpegFillInputBuffer; + sourceMgr.skip_input_data = jpegSkipInputData; + sourceMgr.resync_to_restart = jpegResyncToRestart; + sourceMgr.term_source = jpegTermSource; + cinfo.src = &sourceMgr; + + jpeg_read_header(&cinfo, TRUE); + +#if 0 + //set quality/speed parameters to speed: + cinfo.do_fancy_upsampling = FALSE; + cinfo.do_block_smoothing = FALSE; + + //this actually slows decoding down: + //cinfo.dct_method = JDCT_FASTEST; +#endif + + jpeg_start_decompress(&cinfo); + + dest.resize(cinfo.output_width, cinfo.output_height); + + if(cinfo.num_components == 3) + { + int y = 0; + while(cinfo.output_scanline < cinfo.output_height) + { + //invert image because windows wants it downside up + u8* destBuffer = &dest.getData()[y*dest.getPitch()]; + + //NO idea why jpeglib wants a pointer to a pointer + jpeg_read_scanlines(&cinfo, &destBuffer, 1); + ++y; + } + + //jpeglib gives an error in jpeg_finish_decompress() if no all + //scanlines are read by the application... :-| + //(but because we read all scanlines, it's not really needed) + cinfo.output_scanline = cinfo.output_height; + + } + else + { + //MessageBox(g_hWnd, "Only RGB videos are currently supported.", "oops?", MB_OK); + } + + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + g_isLoading = false; +} diff --git a/thp_player/gcvid.h b/thp_player/gcvid.h new file mode 100755 index 0000000..f0535cc --- /dev/null +++ b/thp_player/gcvid.h @@ -0,0 +1,341 @@ +/*************************************************************************** + * Copyright (C) 2010 + * by thakis + * + * Modification and adjustment for the Wii by Dimok + * + * This software is provided 'as-is', without any express or implied + * warranty. In no event will the authors be held liable for any + * damages arising from the use of this software. + * + * Permission is granted to anyone to use this software for any + * purpose, including commercial applications, and to alter it and + * redistribute it freely, subject to the following restrictions: + * + * 1. The origin of this software must not be misrepresented; you + * must not claim that you wrote the original software. If you use + * this software in a product, an acknowledgment in the product + * documentation would be appreciated but is not required. + * + * 2. Altered source versions must be plainly marked as such, and + * must not be misrepresented as being the original software. + * + * 3. This notice may not be removed or altered from any source + * distribution. + * + * gcvid.h + ***************************************************************************/ + +#ifndef THAKIS_GCVID_H +#define THAKIS_GCVID_H THAKIS_GCVID_H + +#include //FILE* +#include +#include + +//typedef unsigned char boolean; +typedef unsigned char u8; +typedef short s16; +typedef unsigned short u16; +typedef int s32; +typedef unsigned int u32; +typedef long long s64; +typedef float f32; + +#pragma pack(push, 1) + +///////////////////////////////////////////////////////////////////// +//THP + +struct ThpHeader +{ + char tag[4]; //'THP\0' + + //from monk's thp player: + u32 version; //0x00011000 = 1.1, 0x00010000 = 1.0 + u32 maxBufferSize; + u32 maxAudioSamples; //!= 0 if sound is stored in file + + + float fps; //usually 29.something (=0x41efc28f) for ntsc + u32 numFrames; + u32 firstFrameSize; //size of first frame + + u32 dataSize; //size of file - ThpHeader.offset + + //from monk's thp player: + u32 componentDataOffset; //ThpComponents stored here + u32 offsetsDataOffset; //?? if != 0, offset to table with offsets of all frames? + + u32 firstFrameOffset; + u32 lastFrameOffset; +}; + +//monk: + +struct ThpComponents +{ + u32 numComponents; //usually 1 or 2 (video or video + audio) + + //component type 0 is video, type 1 is audio, + //type 0xff is "no component" (numComponent many entries + //are != 0xff) + u8 componentTypes[16]; +}; + +struct ThpVideoInfo +{ + u32 width; + u32 height; + u32 unknown; //only for version 1.1 thp files +}; + +struct ThpAudioInfo +{ + u32 numChannels; + u32 frequency; + u32 numSamples; + u32 numData; //only for version 1.1 - that many + //audio blocks are after each video block + //(for surround sound?) +}; + +//endmonk + + +//a frame image is basically a normal jpeg image (without +//the jfif application marker), the only important difference +//is that after the image start marker (0xff 0xda) values +//of 0xff are simply written as 0xff whereas the jpeg +//standard requires them to be written as 0xff 0x00 because +//0xff is the start of a 2-byte control code in jpeg + +//frame (offsets relative to frame start): +//u32 total (image, sound, etc) size of NEXT frame +//u32 size1 at 0x04 (total size of PREV frame according to monk) +//u32 image data size at 0x08 +//size of one audio block ONLY IF THE FILE HAS SOUND. ThpAudioInfo.numData +//many audio blocks after jpeg data +//jpeg data +//audio block(s) + +struct ThpAudioBlockHeader +{ + //size 80 byte + u32 channelSize; //size of one channel in bytes + u32 numSamples; //number of samples/channel + s16 table1[16]; //table for first channel + s16 table2[16]; //table for second channel + s16 channel1Prev1; + s16 channel1Prev2; + s16 channel2Prev1; + s16 channel2Prev2; +}; + +//audio block: +//u32 size of this audioblock +// +//u32 numBytes/channel of audioblock - that many bytes per channel after adpcm table) +// +//u32 number of samples per channel +// +//2*16 shorts adpcm table (one per channel - always stored both, +//even for mono files), 5.11 fixed point values +// +//4 s16: 2 shorts prev1 and prev2 for each channel (even for mono files) +// +//sound data + +//sound data: +//8 byte are 14 samples: +//the first byte stores index (upper nibble) and shift (lower nibble), +//the following 7 bytes contain 14o samples a 4 bit each + + +///////////////////////////////////////////////////////////////////// +//MTH ("mute thp"?) + +//similar to a thp file, but without sound + +struct MthHeader +{ + //one of the unknown has to be fps in some form + + char tag[4]; //'MTHP' + u32 unknown; + u32 unknown2; + u32 maxFrameSize; + + u32 width; + u32 height; + u32 fps; + u32 numFrames; + + u32 offset; + u32 unknown5; + u32 firstFrameSize; + + //5 padding u32's follow +}; + +//frame: +//u32 size of NEXT frame +//jpeg data + +//see thp (above) for jpeg format. there's a small difference, though: +//mth jpegs end with 0xff 0xd9 0xff instead of 0xff 0xd9 + +#pragma pack(pop) + +//little helper class that represents one frame of video +//data is 24 bpp, scanlines aligned to 4 byte boundary +class VideoFrame +{ +public: + VideoFrame(); + ~VideoFrame(); + + void resize(int width, int height); + + int getWidth() const; + int getHeight() const; + int getPitch() const; + u8* getData(); + const u8* getData() const; + + void dealloc(); + +private: + u8* _data; + int _w; + int _h; + int _p; //pitch in bytes + + //copy constructor and asignment operator are not allowed + //VideoFrame(const VideoFrame& f); + VideoFrame& operator=(const VideoFrame& f); +}; + +//swaps red and blue channel of a video frame +void swapRB(VideoFrame& f); + + +class VideoFile +{ +public: + VideoFile(FILE* f); + virtual ~VideoFile(); + + + virtual int getWidth() const; + virtual int getHeight() const; + virtual float getFps() const; + virtual int getFrameCount() const; + virtual int getCurrentFrameNr() const; + + virtual void loadNextFrame(); + + virtual void getCurrentFrame(VideoFrame& frame) const; + virtual void SetFrameNo( u32 frameNo ); + + //sound support: + virtual bool hasSound() const; + virtual int getNumChannels() const; + virtual int getFrequency() const; + virtual int getMaxAudioSamples() const; + virtual int getCurrentBuffer(s16* data) const; + + +protected: + + FILE* _f; + + //void loadFrame(long offset, int size); + void loadFrame(VideoFrame& frame, const u8* data, int size) const; + +}; + +VideoFile* openVideo(const std::string& fileName); +void closeVideo(VideoFile*& vf); + + +class ThpVideoFile : public VideoFile +{ +public: + ThpVideoFile(FILE* f); + + virtual int getWidth() const; + virtual int getHeight() const; + virtual float getFps() const; + virtual int getFrameCount() const; + + virtual int getCurrentFrameNr() const; + + virtual void loadNextFrame(); + + virtual void getCurrentFrame(VideoFrame& frame) const; + virtual void SetFrameNo( u32 frameNo ); + + virtual bool hasSound() const; + virtual int getNumChannels() const; + virtual int getFrequency() const; + virtual int getMaxAudioSamples() const; + virtual int getCurrentBuffer(s16* data) const; + + +protected: + ThpHeader _head; + ThpComponents _components; + ThpVideoInfo _videoInfo; + ThpAudioInfo _audioInfo; + int _numInts; + + int _currFrameNr; + int _nextFrameOffset; + int _nextFrameSize; + std::vector _currFrameData; +}; + +class MthVideoFile : public VideoFile +{ +public: + MthVideoFile(FILE* f); + + virtual int getWidth() const; + virtual int getHeight() const; + virtual float getFps() const; + virtual int getFrameCount() const; + + virtual int getCurrentFrameNr() const; + virtual void SetFrameNo( u32 frameNo ); + + virtual void loadNextFrame(); + + virtual void getCurrentFrame(VideoFrame& frame) const; + +protected: + MthHeader _head; + + int _currFrameNr; + int _nextFrameOffset; + int _nextFrameSize; + int _thisFrameSize; + std::vector _currFrameData; +}; + +class JpgVideoFile : public VideoFile +{ +public: + JpgVideoFile(FILE* f); + + virtual int getWidth() const; + virtual int getHeight() const; + virtual int getFrameCount() const; + + virtual void getCurrentFrame(VideoFrame& frame) const; + +private: + VideoFrame _currFrame; +}; + +#endif //THAKIS_GCVID_H diff --git a/thp_player/main.cpp b/thp_player/main.cpp new file mode 100644 index 0000000..a7e05d9 --- /dev/null +++ b/thp_player/main.cpp @@ -0,0 +1,12 @@ +#include +#include "thpwindow.h" + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + Q_INIT_RESOURCE( rc ); + ThpWindow w; + w.show(); + + return a.exec(); +} diff --git a/thp_player/next.png b/thp_player/next.png new file mode 100644 index 0000000000000000000000000000000000000000..7c663b380bb5f686b84d31a2eb3792a0239b9419 GIT binary patch literal 5389 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 literal 0 HcmV?d00001 diff --git a/thp_player/pause.png b/thp_player/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..89844fd28a4372ce73f18d5ae0b825b36699e56a GIT binary patch literal 4949 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 F{tt@zFFgPN literal 0 HcmV?d00001 diff --git a/thp_player/rc.qrc b/thp_player/rc.qrc new file mode 100644 index 0000000..a398b44 --- /dev/null +++ b/thp_player/rc.qrc @@ -0,0 +1,16 @@ + + + ffw.png + next.png + pause.png + play.png + prev.png + repeat.png + rev.png + star.png + stop.png + vol_high.png + vol_low.png + vol_med.png + + diff --git a/thp_player/repeat.png b/thp_player/repeat.png new file mode 100644 index 0000000000000000000000000000000000000000..6004c5dc234e1b2ed4d16f999675cd4742b70103 GIT binary patch literal 5629 zcmW+)2{=^!7akEqmI-Ar)}&Dql4LB6j4fj)gp4(ekFjJO)nI6pgluJLd@abHv5(y- zp;CtIWS8L+D&ha@Kl9AJ&%Mtx_ndpqdEfJ%bI-!ekdH@<2Lgfc85`+af%66MQ-^Yb zqj%89327As7NVBJ}si2FZB}2RFGw zjIkK5Q8o@9$;0Jow6hS%F`}_P+UDj&mm?7`ypVm+x3jvo`ZBDqYQE#}>iV(Ny1SQq zxSm-WE5v7@N^C>*`j*oDifLiX<3(6j{(OF;$)nR3UZUpYLi{bSe346;=Z8M`P1Y+A z7UobnjIU5pr!lu9zmH^(M0ij9s?%-X^}-IijLLoAkFE6fo{z|1*xArxote7wo0p&e zRvd0vf&;ArJ=96yMZ=^ivK;7F3KZ23M_bokCAlJsR5M<>CQ16^?p zQ*=G!f{u!c3f^HT8?PSqs-(nk^@_mqpI@62IQW`NrCS%{0`~eDW2;NhL)sKsqf`zw z3`Uk=So9_hG@8T7DRTF?PElkV$vsv0#xj(amR2;`DssJ0YH#r%<|aBq{~)CHZ_i=E@B9T zmR*qzztVLYewF=*Z(v}BJ06eU$+_UCQ?~NkabWWuNqR9=P6m2N-;x76DBY_H0$lj9 zwX>5$US3{VTl>Mth=Ya6v*Obs&W1%c?d|bLD#JJ2z6D+Csk$!>1#f|T{vkAX&osoj z_N1}c8jGbXM4qPavRV$tcKMf?E&E%)y{hXM${N?c!pXQ(kXWnYBx<5Gv|vTk#Ri>daCR14%~W~kZ|EKKjO2L^>~#;FxSb0$B2{BRv_2t^)G zN}Pqq!4j!Z?BG;`5)~;+#@Pf+G)0V6y>5P@?ytFJ$Q3&G?s4A(gV_K(V4y2>JP1zK zgxt&_1Is>QioNY4)!u~FVU_#I$yA%uxWO0Ly4|%#%OaZ;vzKfTDl!OBUzX%sL)qVt zd8$Jc+uPf-uW)6f;YUb@QbghBR@`*f35qO|(oQ#&GcL9(`7*l|No39UJu%;FL-X(V|=elAT*COiRV%8=XStuE6vMH9i93iZI5) zM4?k= z(z#~tyW8J~E8V$XE@&hlWvF;K%$pi0#4GkD&SqtnDfT|?c4mSrdm3K!_*us|;GFBt zo6yX2Hw+97xp?9a4P?j}t3J00@bj}yNJ!Wt?r!Jq-Hz1^{+;KT!J zOko_Lf+i34cL+K-b+9$^RP96rK+N6z?zn!ihZrKu{`+V&V4*1D;c z-s}0Mw$?p58gc&o`9&6sLB%Ac^m(z}!C|+FH~KtOzk2KLjx6uccbcb%;GL-6WLOm{ z*Q|U?YXIaZ-O9pbdRNCKdIRIs5+ec1LR(uqe7%k9Qh}!fXNQw824P*LSU7@r^YDl} ze^TiB@tX)niX1nEdIv@-D^tRcHyrc#_gBUWoumJh?Pf6QUri3Fv$r}rWQ5(Q3w$

so-{u6T$m(1wfM0RIBTer&zR{-s`MF@&y%pVt&er}WE^tS zh+HYHRo2i*)R%2zcVqTey{|rPEEqk>D7G!Bh}`l8ul&Bhf9CNum%FnX(2kUBxAD!U zAf?PvjZ2s6XSHJ+2(tzW(ok_gzyGE$lcZZiosClCj)*8tt&UV9ADi?nY}T>pyBfBB zIh-JdPc=G(guX`W2mmZbNXBd=-IxNU*W*>a`Pvf3VPSg=0!U@R(cE01YDDqfoT`C= zK_CELjP`S3OW~!sl>QLSy`HWl5tdtJPty-_z&4WdNt98ER=~hptgT-7%yixp0wE*g zT;Haiw10E+C?FiQ2eshOXkpB&Vv2_K7!lUc6n( z#U;TG8K+)`qIL2OuU!)SYBI`OkSwb8!+fC?q!3O?TaTM}?M9M}LMPo4$bk&NngEzSAj>vjDq91>KIFnKPR(bKUtB~2)=|S=IsI5q_R-WC1!KV*Z)3Rx+yE;P z5FKjR=w<8t_641tO8`q*FYaY$i^B9Of)~v26HTHSo`eLTY6KEJ6S;L7TX1j-;lnvE zo>?GU)vzGEgx%05aX|Q-9vmDTr~2fKr++emlQ~zs+m*jbgDx?9;*rrhuW@Qzh~|Tm zYvnVai>!_0)3WmNx=-nBa7VAYOI<1a=<3f7@VqwOSQ)YDr6$I=CL`*n+U%yJ!+Ry! z!R}I#4NpT^aQ!R0lBNS566Et{sD33b`vKJys$gL0VPO)w4Kq%4bvVwHn&*ILIb;Z9 z+A7@{Fw9ZYL4AObzfA`8T*%XBryuhG#Lk0HwzK>&owd1J!`Xf4ny}|YdvP((C~Z1I zl10NnpUTX6Ro_EWE-TC_ToaCGYBh=u%Gt%-(?2vjI}4i8%n0$%2IKm;8sq(U+TF%r z+V9fiMWOUI`;@~cjRhGhgBM0=XlO+3UJI65wxSo}EK=&pfX9yzZv|s4=ljHgPyl)a zU>uMF&>-&~pPAO+AAl9EDDzG?C<$Yxsc)+$C^C~f3unioL_Nu8=dX%#DHC>xQmVV| ziu9Enhoy>Rz8CTo*(p?4H@8D^Fi_i0N?aYvAMonGcxuZu8ebL{{}C;fOsP4c2)BEI z$A6%x3~5v?AG)J)|7Uy8>lcSHxi6PrPdNBgOxEl-kKq3cGL}!%T153G5g%?@myWp2 zWg5xVod^9pI5-#}uxzEj(R<}jMd;H9i{staUS>{$6GS%o*KsKkz5^!roPl`I8klWP zDK38MYEK((wl}sixg_(3hA3N^najLkP-=GtPG0=}y^D1CNGpOrN-|iixVYclT!wS! zp%&61{im-g(KWd)6yq51d1$D+8l|dwXp|NKS!LrbEG-D|RNtn?!B$6W1ppN=L-CXJ z+=u;e#o^t`c%PG!$J5JJU=C4z@Dy(4Gl#vh9f{(NwCd_K0UI)ztgNQiiS;j;cr#OY z%zO2KjGF>2MmkP9+mvcmGm<#QPpN6<@@z#P>Y zV3&!j70FxJI9FxscCp{9dYI-fuGp(5pVoG!yoq#iGFs?Ybl$`EEOyt9Oq9P}R20f3 za_Qt>rmj+qwW8#r!h#~bk`^N*fyXNiO?|lcM3j5yrdFf9;!tOQf4_<|ldE0%b@dYR zy0gJY{5)1+F8c@xvK1-OcsPiyYY0Up&u%0qC$k-eP`YML>h;vI zcH_Pj`H%K^r~-MBJR4}~xpl@^5W%=xT>NvWmfq!a8`Q74&M&!`AB9qdrKNE+nk-Me z?58Eg5bb+<&l24#InXKV0SDWxpWPSf+yjLLYWhWQAUF1wCr|LYuZc@AroYTID1kiS z;2=l*bwp?FCw=^i*DIq<58d(3i|CHno#~Q9!!&0#%*Z%s{%^$nvK85E(;!yYI@L!E zD)U#RfmV_%sr_#0_xkjS@Xfx9AMxrA-h6{|FdaBi{L4(;W$q03^mE_J1u@ z0@(q$PX;%Ac?S!D#d>XeLnkM7^Xb{J-Or5)d#wKm;lm zFD3vQ3(@R5-x;{HOV2fuBVQ|bA;wIo`E*f^WjZk>1W0$3O-E>bh75r6bu9sK40O)a zO?f=~{ylGGRMf_DNx9qReBa+L20Be$usT+##_<6v?#aafs!Mqx5YdLoh&D2xxblSy z@yfht=-riu3jZgW`(O~_Kwm!nnBOD?Ch!fyT zrkDL{#3)^|%Uha)MRFo$@;#|?==76o|6RazT@lcZGn9;kJ%ps$aKNnm6j-C2nEzls2JB^qXfv5a;f z$kRU9YQIf)-7|PG>f1=@I-;al$iI1I|T@0&7I)U<2!^TpPphAY4oP~^2@dKMm{^Jc>Q`$S@sBKi3 zcFcOLl)bs`o?5-Sa)lEt?iL!BdOrg@sE=TP!8fbu3(SHd&0ELq#RlN1TTp<8%tREh z#VMyr+!H~&9vXbtG(tFvty^tw&K;4Q5TcQp19Savp~ghq`QTOXNB?PEmT&C z+c$(PwR!LVdZN4E942_MB(=YB228q9Hnz@Zj8prc)&M<}1|}8dWDBJ5hoK=dm?#)N z#l?D79`vwL%}$T|&^y)_2P-3+`A2DwdOa-5nJOykIsxjhkUTPRHo^5{&&qG_txUSq zcx|z2mpCE1Ut4aketLd27Xa^=Lq@1c(fj$J2L|lb)zwDNt&C1u2R^~R@9S^hiH1VP zM$-`=AoPVcn&NN<-;7`#R)nvbjd9DhKl|0dXBF!b+zWN~5*!@40B6H2$zY}Jy z68@B&&%g70CXQhmS=}(bx;~uf)9mcOGCdCN!BqAhil2;X>h;{xyO&&-xTbIs;j8=XZvL=BH0^G1xnkK6^jBPkIKp|rSoP8#}S zNW^`_-TE_-ve?0S{|8mKCU%HF=>ok~b$hsGJHM8b6xpL3ZSrxv=Uf`Ty=T(5Yls3` z>#e)Gh)=)&@~>3XR%_e%3!i^X<Kt^dvSnuEb{;pdGX`<@5+-)@fG&Ad^+ z(t4Nux{^!NPa|Q>I2CzIUXc4rhUXqF4G)ncTh&{=Bs%v8aE(>V6=sjn%Kw>Gb~n{r&&K zL#d4;c)Ogw7i~A6eCIAH_VvqAcPX;r)I_05fEyAqQ+K&IBO@dC|1hw(nG4+y#3g|J vaUkh2>bH_+;XuHQ;AwVBnYwRc+YY$WCvIOW{__kAcC!#;jG2C!o-65p`N^~* literal 0 HcmV?d00001 diff --git a/thp_player/rev.png b/thp_player/rev.png new file mode 100644 index 0000000000000000000000000000000000000000..f1e9ceaf2a6533348df56b64e67042075ac93d71 GIT binary patch literal 5332 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 literal 0 HcmV?d00001 diff --git a/thp_player/star.png b/thp_player/star.png new file mode 100644 index 0000000000000000000000000000000000000000..e209cd4dbd117ffc0dfa10cf309c5094cb4069a6 GIT binary patch literal 5449 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⪙E<2?n1Ofvl2@4z+@`swCoNBYW%PKf)TXOhy9{c#m`b6oDC14*ECZ|ZGiR`6k^|B`$|yHX+=}@-rEBZH_2z| zstF_;^Vv5C=3wAEE_`I|prEZ)CZfiMV(#(DtpEoHhqL4i{zK~DFJCeZ3=DYPZ9U`$ zw$v&sD}g<;yM0?5*51_xbw#78h3v>aS*Vd4g@|9QT+)L%<9k!e?e(AEi1RKolmuC1 z0mBtnsiMpiF_XaejZk8bw@yegw$^dvBAcRpex4ab%aP61n(?Rn?cc zF57h-oQAxxxHvpEM$t!SFDSbb%Gty@4@^4b&>(%44iRPK@lQy=3|E^=>wc3-IOJMc zOH`)%QFqne$wbu!(saZgZ}J(z;?Ud~bI3-N_o* zF2=6)?C=IUj46M9wB3^{C;NnrOUef92Q(;^FH1+(LI;x?roVh)^YHK}oVw+!x2bl9 z9Q53~=wr+Z@kg>iaP->JQOXp$f$ND*>j$SuT zEDo4TE@g;`%KKW?I)hO=u3%kcDqIK0fKGR#Wt4gf{Qc#55;?Lw@FOa6cLfOE?OMl= z$Df%rTbdf|rCA9(dKl)bLSYJ(5XA75w)`{KNaF&deaIPxT~TFWahFK-J#LsL;vxf8jZX7+LKwq~}Hf&%gAaQfu9T+#pgc-a36d`W>v zNrtIO8n<4mGidv+n`XWkoyRHg6BK2im<*1N=HK=8jq;_Gsh`w)Oik>?RwWxzo@&N! zFAkL#`A1Rz{!4+4l)~mG(ae%> zEn$?olPYrDl5e<|7v}Ez9}GCW`Jp5y_cquq`0a?yqe6FE95@8^_ttADfL1+lM$ykE*5@dRJ?5ltKX%CW)|#U%FrF m8CbbWNU`16*6Gos6QtJxs2{A&M>ecEv#s442s9)d! literal 0 HcmV?d00001 diff --git a/thp_player/stop.png b/thp_player/stop.png new file mode 100644 index 0000000000000000000000000000000000000000..807cf24cf7146081beeae43490677ec1bf6e4d25 GIT binary patch literal 5003 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 literal 0 HcmV?d00001 diff --git a/thp_player/thp_player.pro b/thp_player/thp_player.pro new file mode 100644 index 0000000..37fae8e --- /dev/null +++ b/thp_player/thp_player.pro @@ -0,0 +1,26 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2011-01-05T10:05:50 +# +#------------------------------------------------- + +QT += core gui multimedia + +TARGET = thp_player +TEMPLATE = app + + +SOURCES += main.cpp\ + thpwindow.cpp \ + gcvid.cpp + +HEADERS += thpwindow.h \ + gcvid.h + +FORMS += thpwindow.ui + +CONFIG += static +LIBS += -ljpeg + +RESOURCES += \ + rc.qrc diff --git a/thp_player/thpwindow.cpp b/thp_player/thpwindow.cpp new file mode 100644 index 0000000..db383c2 --- /dev/null +++ b/thp_player/thpwindow.cpp @@ -0,0 +1,376 @@ +#include "thpwindow.h" +#include "ui_thpwindow.h" +#include "../WiiQt/tools.h" + +#define MAX_BUFFERS 50 //in frames +#define BUFFER_DELAY 200 //time to wait between checking buffer size ( msecs ) + +ThpWindow::ThpWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::ThpWindow) +{ + ui->setupUi( this ); + ui->mainToolBar->setVisible( false ); + ui->label_fName->clear(); + ui->label_fpsT->clear(); + ui->label_itemNo->clear(); + ui->label_sizeT->clear(); + ui->label_timeCur->clear(); + ui->label_timeFull->clear(); + ui->label_video->clear(); + ui->scrollArea->setWidgetResizable( false ); + EnableGui( false ); + ui->statusBar->addPermanentWidget( ui->label_fName, 0 ); + ui->statusBar->addPermanentWidget( ui->label_itemNo, 0 ); + + AudioOutputDevice = NULL; + AudioOutput = NULL; + videoFile = NULL; + dontBuffer = false; + //currentDir = QDir::currentPath(); + currentDir = "/media/Jenna/jenna/c/gui_fork/SSBB_JAP/DATA/files/movie"; + + //workaround for some bullshit bug in the Qt libs + foreach( const QAudioDeviceInfo &deviceInfo, QAudioDeviceInfo::availableDevices( QAudio::AudioOutput ) ) + { + qDebug() << "l:" << deviceInfo.deviceName(); + } + + connect( &timer, SIGNAL( timeout() ), this, SLOT( ShowNextFrame() ) ); + +} + +ThpWindow::~ThpWindow() +{ + dontBuffer = true; + timer.stop(); + delete ui; + if(videoFile) + { + closeVideo( videoFile ); + videoFile = NULL; + } + if( AudioOutput ) + { + AudioOutput->stop(); + delete AudioOutput; + AudioOutput = NULL; + } +} + +//open file +void ThpWindow::on_actionOpen_triggered() +{ + timer.stop(); + dontBuffer = true; + playList.clear(); + curPlayListPos = 0; + QStringList fNames = QFileDialog::getOpenFileNames( this, tr("Open Files"), currentDir, tr("Wii/GC Videos (*.thp *.mth)") ); + if( fNames.isEmpty() ) + return; + + foreach( QString str, fNames ) + playList << str; + + currentDir = QFileInfo( playList.at( 0 ) ).absolutePath(); + + PlayPlayListItem( 0 ); +} + +//open folder +void ThpWindow::on_actionOpen_Folder_triggered() +{ + timer.stop(); + dontBuffer = true; + playList.clear(); + curPlayListPos = 0; + QString dirName = QFileDialog::getExistingDirectory( this, tr( "Open Folder" ), currentDir ); + if( dirName.isEmpty() ) + return; + + currentDir = dirName; + + QDir dir( dirName ); + QFileInfoList fil = dir.entryInfoList( QStringList() << "*.thp" << "*.mth" , QDir::Files ); + //qDebug() << "found" << fil.size() << "items"; + foreach( QFileInfo fi, fil ) + playList << fi.absoluteFilePath(); + + PlayPlayListItem( 0 ); +} + +void ThpWindow::PlayPlayListItem( quint32 i ) +{ + //qDebug() << "ThpWindow::PlayPlayListItem" << i; + if( !playList.size() ) //no videos to play + { + ui->label_itemNo->clear(); + return; + } + + if( i >= (quint32)playList.size() ) //all videos are played + { + curPlayListPos = 0; //skip back to beginning + if( !ui->pushButton_loop->isChecked() ) //dont loop + { + ui->pushButton_playPause->setChecked( false ); + timer.stop(); + dontBuffer = true; + } + } + + ui->label_itemNo->setText( QString( "%1 / %2").arg( curPlayListPos + 1 ).arg( playList.size() ) ); + LoadVideo( playList.at( curPlayListPos ) ); +} + +void ThpWindow::LoadVideo( const QString &path ) +{ + //qDebug() << "ThpWindow::LoadVideo" << path; + EnableGui( false ); + std::string filepath = path.toUtf8().constData(); + + ui->progressBar_buffer->setValue( 0 ); + + //stop current video + timer.stop(); + if( videoFile ) + { + closeVideo( videoFile ); + videoFile = NULL; + } + + Frames.clear(); + SoundBuffers.clear(); + + videoFile = openVideo( filepath ); + if( !videoFile ) + { + QMessageBox::information( this, tr("Player"), tr("Cannot load %1.").arg( path ) ); + return; + } + + //dontBuffer = false; + ui->label_video->setFixedSize( videoFile->getWidth(), videoFile->getHeight() ); + frameCnt = videoFile->getFrameCount(); + curFrame = 0; + for( quint8 i = 0; i < 3; i++ ) + LoadNextFrame(); + + CreateAudioOutput(); + + //show some info in the gui + ui->label_fpsT->setText( QString( "%1" ).arg( videoFile->getFps(), 0, 'f', 3 ) ); + ui->label_sizeT->setText( QString( "%1 x %2" ).arg( videoFile->getWidth() ).arg( videoFile->getHeight() ) ); + ui->label_fName->setText( QFileInfo( path ).fileName() ); + ui->horizontalSlider_pos->setMaximum( frameCnt ); + + //set timer for animation + qreal delay = 1000.0f/videoFile->getFps(); + ui->label_timeFull->setText( TimeTxt( delay * videoFile->getFrameCount() )); + timer.setInterval( delay ); + + //if play button is clicked, just play + if( ui->pushButton_playPause->isChecked() ) + timer.start(); + //otherwise just load the first frame + else + ShowNextFrame(); + + //allow the buttons to work + EnableGui( true ); +} + +QString ThpWindow::TimeTxt( quint64 msecs ) +{ + quint32 hours = msecs / 3600000; + msecs -= ( hours * 3600000 ); + quint32 minutes = msecs / 60000; + msecs -= ( minutes * 60000 ); + quint32 seconds = msecs / 1000; + msecs -= ( seconds * 1000 ); + return QString( "%1:%2:%3.%4" ).arg( hours, 2, 10, QChar( '0' ) ) + .arg( minutes, 2, 10, QChar( '0' ) ) + .arg( seconds, 2, 10, QChar( '0' ) ) + .arg( msecs, 3, 10, QChar( '0' ) ); +} + +void ThpWindow::LoadNextFrame() +{ + //qDebug() << "ThpWindow::LoadNextFrame()"; + VideoFrame VideoF; + videoFile->loadNextFrame(); + videoFile->getCurrentFrame(VideoF); + QImage image(VideoF.getData(), VideoF.getWidth(), VideoF.getHeight(), QImage::Format_RGB888); + if (image.isNull()) + return; + + Frames.push_back(QPixmap::fromImage(image)); + + + int SoundPos = SoundBuffers.size(); + SoundBuffers.resize(SoundBuffers.size()+1); + SoundBuffers[SoundPos].Buffer.resize(videoFile->getMaxAudioSamples()*2); + SoundBuffers[SoundPos].Size = videoFile->getCurrentBuffer(&SoundBuffers[SoundPos].Buffer[0])*2*2; +} + +void ThpWindow::ShowNextFrame() +{ + //qDebug() << "ThpWindow::ShowNextFrame()" << Frames.size() << curFrame << frameCnt; + if( Frames.size() < 3 ) + { + BufferIfNeeded(); + return; + } + if( ++curFrame >= frameCnt ) //end of video + { + PlayPlayListItem( ++curPlayListPos ); + return; + } + + ui->horizontalSlider_pos->setValue( curFrame ); + qreal delay = 1000.0f/videoFile->getFps(); + ui->label_timeCur->setText( TimeTxt( delay * videoFile->getCurrentFrameNr() ) ); + + ui->label_video->setPixmap(Frames[2]); + //ui->label_video->setPixmap(Frames[0]); + + if( AudioOutputDevice && ui->pushButton_vol->isChecked() ) + //&& SoundBuffers.size() > 2 + //&& SoundBuffers[ 2 ].Buffer.size() + //&& SoundBuffers[ 2 ].Size ) + AudioOutputDevice->write((char *) &SoundBuffers[2].Buffer[0], SoundBuffers[2].Size); + + Frames.erase(Frames.begin()); + SoundBuffers.erase(SoundBuffers.begin()); +} + +void ThpWindow::BufferIfNeeded() +{ + + if( dontBuffer ) + return; //break the buffer loop + + if( Frames.size() < MAX_BUFFERS )//we need to read a frame + { + LoadNextFrame(); + } + + //show buffer in the gui + int b = ((float)Frames.size() / (float)MAX_BUFFERS) * 100.0f; + ui->progressBar_buffer->setValue( b ); + + //wait a bit and call this function again + QTimer::singleShot( BUFFER_DELAY, this, SLOT( BufferIfNeeded() ) ); +} + +void ThpWindow::CreateAudioOutput() +{ + //qDebug() << "ThpWindow::CreateAudioOutput()" << timer.isActive(); + + if( AudioOutput ) + { + AudioOutput->stop(); + delete AudioOutput; + AudioOutput = NULL; + } + AudioOutputDevice = NULL; + + AudioFormat.setFrequency( videoFile->getFrequency() ); + AudioFormat.setChannels( videoFile->getNumChannels() ); + AudioFormat.setSampleSize( 16 ); + AudioFormat.setCodec( "audio/pcm" ); + AudioFormat.setByteOrder( QAudioFormat::LittleEndian ); + AudioFormat.setSampleType( QAudioFormat::SignedInt ); + + QAudioDeviceInfo info( QAudioDeviceInfo::defaultOutputDevice() ); + if( !info.isFormatSupported( AudioFormat ) ) + { + AudioFormat = info.nearestFormat( AudioFormat );//try to find a usable audio playback format + if( !info.isFormatSupported( AudioFormat ) ) + { + qWarning() << "unsupported audio format: can't play anything"; + ui->statusBar->showMessage( tr( "Can't find suitable audio format" ), 5000 ); + return; + } + } + AudioOutput = new QAudioOutput( AudioFormat, this ); + if( !AudioOutput ) + { + ui->statusBar->showMessage( tr( "Audio output error" ), 5000 ); + qWarning() << "!AudioOutput"; + return; + } + AudioOutputDevice = AudioOutput->start(); + if( AudioOutput->error() ) + { + ui->statusBar->showMessage( tr( "Audio output error" ), 5000 ); + qWarning() << "AudioOutput->error()" << AudioOutput->error(); + AudioOutput->stop(); + AudioOutputDevice = NULL; + } +} + +//enable/disable buttons +void ThpWindow::EnableGui( bool enable ) +{ + ui->pushButton_ffw->setEnabled( enable ); + ui->pushButton_loop->setEnabled( enable ); + ui->pushButton_next->setEnabled( enable ); + ui->pushButton_playPause->setEnabled( enable ); + ui->pushButton_prev->setEnabled( enable ); + ui->pushButton_rewind->setEnabled( enable ); + ui->pushButton_stop->setEnabled( enable ); + ui->pushButton_vol->setEnabled( enable ); +} + +//play button +void ThpWindow::on_pushButton_playPause_clicked() +{ + if( ui->pushButton_playPause->isChecked() ) + { + dontBuffer = false;//start buffering again after stopped + timer.start(); + } + else + { + //dontBuffer = true;//ok to buffer while paused + timer.stop(); + } +} + +//next button +void ThpWindow::on_pushButton_next_clicked() +{ + PlayPlayListItem( ++curPlayListPos ); +} + +//prev button +void ThpWindow::on_pushButton_prev_clicked() +{ + if( !curPlayListPos ) + curPlayListPos = playList.size(); + PlayPlayListItem( --curPlayListPos ); +} + +//stop button +void ThpWindow::on_pushButton_stop_clicked() +{ + //stop playback + timer.stop(); + ui->pushButton_playPause->setChecked( false ); + + //clear buffer + dontBuffer = true; + Frames.clear(); + SoundBuffers.clear(); + ui->progressBar_buffer->setValue( 0 ); + + //set video to first frame + videoFile->SetFrameNo( 0 ); + + //read a few frames into buffer + curFrame = 0; + for( quint8 i = 0; i < 3; i++ ) + LoadNextFrame(); + + //show first frame in gui + ShowNextFrame(); +} diff --git a/thp_player/thpwindow.h b/thp_player/thpwindow.h new file mode 100644 index 0000000..6f0f2da --- /dev/null +++ b/thp_player/thpwindow.h @@ -0,0 +1,73 @@ +#ifndef THPWINDOW_H +#define THPWINDOW_H + +#include +#include +#include "gcvid.h" +#include "../WiiQt/includes.h" + +namespace Ui { + class ThpWindow; +} + +typedef struct +{ + std::vector< qint16 > Buffer; + int Size; +} SoundFrame; + +class ThpWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit ThpWindow( QWidget *parent = 0 ); + ~ThpWindow(); + + void LoadVideo( const QString &path ); + +private: + Ui::ThpWindow *ui; + void LoadNextFrame(); + + VideoFile * videoFile; + QAudioOutput* AudioOutput; + QIODevice* AudioOutputDevice; + //QBuffer AudioOutputDevice; + QAudioFormat AudioFormat; + std::vector Frames; + std::vector SoundBuffers; + + void CreateAudioOutput(); + + QTimer timer; + bool dontBuffer; + + QString TimeTxt( quint64 msecs ); + quint64 frameCnt; + quint64 curFrame; + + QStringList playList; + quint32 curPlayListPos; + void PlayPlayListItem( quint32 i ); + QString currentDir; + + void EnableGui( bool enable = true ); + QSize MaxSizeForRatio( qreal w, qreal h ); + +private slots: + //void on_actionFit_To_Window_triggered(bool checked); + void on_pushButton_stop_clicked(); + void on_pushButton_prev_clicked(); + void on_pushButton_next_clicked(); + void on_actionOpen_Folder_triggered(); + void on_pushButton_playPause_clicked(); + void ShowNextFrame(); + void BufferIfNeeded(); + void on_actionOpen_triggered(); + +//protected: + //void resizeEvent ( QResizeEvent * event ); +}; + +#endif // THPWINDOW_H diff --git a/thp_player/thpwindow.ui b/thp_player/thpwindow.ui new file mode 100644 index 0000000..04e6736 --- /dev/null +++ b/thp_player/thpwindow.ui @@ -0,0 +1,485 @@ + + + ThpWindow + + + + 0 + 0 + 644 + 714 + + + + ThpWindow + + + + + 2 + + + 2 + + + + + + 640 + 520 + + + + true + + + Qt::AlignCenter + + + + + 0 + 0 + 636 + 516 + + + + + 0 + + + 0 + + + + + + 0 + 0 + + + + video goes here + + + true + + + Qt::AlignCenter + + + + + + + + + + + 2 + + + QLayout::SetDefaultConstraint + + + + + 2 + + + + + 0 + + + + + false + + + + 486 + 0 + + + + Qt::Horizontal + + + false + + + false + + + + + + + curTime + + + Qt::AlignCenter + + + + + + + + + 2 + + + + + + + + + :/prev.png:/prev.png + + + + 36 + 36 + + + + false + + + false + + + + + + + + + + + :/rev.png:/rev.png + + + + 36 + 36 + + + + false + + + false + + + + + + + + + + + :/play.png + :/pause.png:/play.png + + + + 36 + 36 + + + + true + + + false + + + + + + + + + + + :/stop.png:/stop.png + + + + 36 + 36 + + + + false + + + false + + + + + + + + + + + :/ffw.png:/ffw.png + + + + 36 + 36 + + + + false + + + false + + + + + + + + + + + :/next.png:/next.png + + + + 36 + 36 + + + + false + + + false + + + + + + + + + + + :/repeat.png:/repeat.png + + + + 36 + 36 + + + + true + + + false + + + + + + + Qt::Horizontal + + + + 20 + 20 + + + + + + + + + + + + :/vol_low.png + :/vol_high.png:/vol_low.png + + + + 36 + 36 + + + + true + + + true + + + + + + + + + + + 2 + + + QLayout::SetFixedSize + + + + + totalTime + + + Qt::AlignCenter + + + + + + + QLayout::SetMinAndMaxSize + + + QFormLayout::AllNonFixedFieldsGrow + + + 2 + + + 2 + + + + + FPS: + + + + + + + fps + + + + + + + Size: + + + + + + + x x y + + + + + + + Buffer + + + + + + + 0 + + + + + + + + + + + + + + + + TextLabel + + + + + + + TextLabel + + + + + + + + + 0 + 0 + 644 + 27 + + + + + File + + + + + + + + + TopToolBarArea + + + false + + + + + + Open File + + + Ctrl+O + + + + + Open Folder + + + Ctrl+P + + + + + + + + + diff --git a/thp_player/vol_high.png b/thp_player/vol_high.png new file mode 100644 index 0000000000000000000000000000000000000000..a1ed330afe37b04b73e24aeca04bed4ed7dbdf2e GIT binary patch literal 5682 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