//----------------------------------------------------------------------------- // Torque Game Engine // Copyright (C) GarageGames.com, Inc. //----------------------------------------------------------------------------- #include "theoraPlayer.h" #include "math/mMath.h" #include "util/safeDelete.h" #include "platform/profiler.h" //----------------------------------------------------------------------------- TheoraTexture::MagicalTrevor::BufInf::BufInf() { id=0; time=0; next=NULL; } TheoraTexture::MagicalTrevor::MagicalTrevor() { mBufListHead = NULL; mLastBufferID = -1; mMutex = Mutex::createMutex(); } TheoraTexture::MagicalTrevor::~MagicalTrevor() { // Cheat on freeing, since BufInf has no destructor. mBuffPool.freeBlocks(); Mutex::destroyMutex(mMutex); } const F64 TheoraTexture::MagicalTrevor::advanceTime(const ALuint buffId) { MutexHandle handle; handle.lock(mMutex); // We basically find the last entry on the list that references // this buffId, then count how much time it + all its followers // contains, and return that amount. Then the list is truncated. // Skip if we just saw this one... we'd better not go // through all the buffers in one advanceTime call, that would // confuse the hell out of this code. if(mLastBufferID == buffId) return 0.f; mLastBufferID = buffId; // Ok, find last occurence of buffId. BufInf **walk = &mBufListHead; BufInf **lastOccurence = NULL; while(*walk) { if((*walk)->id == buffId) lastOccurence = walk; walk = &(*walk)->next; } if(lastOccurence == NULL) { Con::warnf("MagicalTrevor::advancetime - no last occurrence for buffer %d found!", buffId); return 0.f; } // We've got the last occurrence, sum the time and truncate the list. F64 timeSum = 0.f; walk = lastOccurence; while(*walk) { timeSum += (*walk)->time; // Blast it and advance. BufInf *del = *walk; *walk = (*walk)->next; mBuffPool.free(del); } return timeSum; } void TheoraTexture::MagicalTrevor::postBuffer(const ALuint buffId, const F64 duration) { MutexHandle handle; handle.lock(mMutex); // Stick the buffer at the front of the queue... BufInf *walk = mBuffPool.alloc(); walk->id = buffId; walk->time = duration; // Link it in. walk->next = mBufListHead; mBufListHead = walk; } const U32 TheoraTexture::MagicalTrevor::getListSize() const { MutexHandle handle; handle.lock(mMutex); U32 size=0; // Ok, find last occurence of buffId. const BufInf *walk = mBufListHead; while(walk) { size++; walk = walk->next; } return size; } void TheoraTexture::MagicalTrevor::reset() { MutexHandle handle; handle.lock(mMutex); // Since we're mostly touched by the thread, let's make this a mutex // operation. mBuffPool.freeBlocks(); mBufListHead = NULL; mLastBufferID = -1; } //----------------------------------------------------------------------------- TheoraTexture::TheoraTexture() { init(); } TheoraTexture::TheoraTexture(const char* szFilename, bool fPlay, Audio::Description* desc) { init(); setFile(szFilename, fPlay, false, desc); } void TheoraTexture::init() { mReady = false; mPlaying = false; mHasVorbis = false; mVorbisHandle = NULL; mVorbisBuffer = NULL; mPlayThread = NULL; mTheoraFile = NULL; mTextureHandle = NULL; mMagicalTrevor.reset(); } TheoraTexture::~TheoraTexture() { destroyTexture(); } // tears down anything the texture has void TheoraTexture::destroyTexture(bool restartOgg) { mPlaying = false; // kill the thread if its playing SAFE_DELETE(mPlayThread); // kill the sound if its playing if(mVorbisHandle) { alxStop(mVorbisHandle); mVorbisHandle = NULL; mVorbisBuffer = NULL; // this is already deleted in alxStop mMagicalTrevor.reset(); } if(mHasVorbis) { ogg_stream_clear(&mOggVorbisStream); dMemset(&mOggVorbisStream, 0, sizeof(ogg_stream_state)); vorbis_dsp_clear(&mVorbisDspState); dMemset(&mVorbisDspState, 0, sizeof(vorbis_dsp_state)); vorbis_block_clear(&mVorbisBlock); dMemset(&mVorbisBlock, 0, sizeof(vorbis_block)); vorbis_comment_clear(&mVorbisComment); vorbis_info_clear(&mVorbisInfo); mHasVorbis = false; mMagicalTrevor.reset(); } if(mReady) { ogg_stream_clear(&mOggTheoraStream); theora_clear(&mTheoraState); theora_comment_clear(&mTheoraComment); theora_info_clear(&mTheoraInfo); ogg_sync_clear(&mOggSyncState); } // close the file if it's open if(mTheoraFile) { ResourceManager->closeStream(mTheoraFile); mTheoraFile = NULL; } if(restartOgg) return; // Set us to a null state. mReady = false; //SAFE_DELETE(mTextureHandle); } // Takes file name to open, and whether it should autoplay when loaded bool TheoraTexture::setFile(const char* filename, bool doPlay, bool doRestart, Audio::Description* desc) { if(mPlaying) stop(); if(mReady) destroyTexture(doRestart); // open the theora file mTheoraFile = ResourceManager->openStream(filename); mMagicalTrevor.reset(); if(!mTheoraFile) { Con::errorf("TheoraTexture::setFile - Theora file '%s' not found.", filename); return false; } Con::printf("TheoraTexture - Loading file '%s'", filename); // start up Ogg stream synchronization layer ogg_sync_init(&mOggSyncState); // init supporting Theora structures needed in header parsing theora_comment_init(&mTheoraComment); theora_info_init(&mTheoraInfo); // init supporting Vorbis structures needed in header parsing vorbis_comment_init(&mVorbisComment); vorbis_info_init(&mVorbisInfo); if(!parseHeaders()) { // No theora stream found (must be a vorbis only file?) // Clean up all the structs theora_comment_clear(&mTheoraComment); theora_info_clear(&mTheoraInfo); // trash vorbis too, this class isn't for playing lone vorbis streams vorbis_info_clear(&mVorbisInfo); vorbis_comment_clear(&mVorbisComment); Con::errorf("TheoraTexture::setFile - Failed to parse Ogg headers"); return false; } // If theora stream found, initialize decoders... theora_decode_init(&mTheoraState, &mTheoraInfo); // This is a work around for a bug in theora when you're using only the // decoder (think its fixed in newest theora lib). mTheoraState.internal_encode = NULL; // Note our state. Con::printf(" Ogg logical stream %x is Theora %dx%d %.02f fps video", mOggTheoraStream.serialno, mTheoraInfo.width, mTheoraInfo.height, (F64)mTheoraInfo.fps_numerator / (F64)mTheoraInfo.fps_denominator); Con::printf(" - Frame content is %dx%d with offset (%d,%d).", mTheoraInfo.frame_width, mTheoraInfo.frame_height, mTheoraInfo.offset_x, mTheoraInfo.offset_y); if(mHasVorbis) { vorbis_synthesis_init(&mVorbisDspState, &mVorbisInfo); vorbis_block_init(&mVorbisDspState, &mVorbisBlock); Con::printf(" Ogg logical stream %x is Vorbis %d channel %d Hz audio.", mOggVorbisStream.serialno, mVorbisInfo.channels, mVorbisInfo.rate); if(!(mHasVorbis = createAudioBuffers(desc))) { ogg_stream_clear(&mOggVorbisStream); vorbis_block_clear(&mVorbisBlock); vorbis_dsp_clear(&mVorbisDspState); } } // Check again because the buffers might fail. if(!mHasVorbis) { // no vorbis stream was found, throw out the vorbis structs vorbis_info_clear(&mVorbisInfo); vorbis_comment_clear(&mVorbisComment); } if(!mReady) { if(!createVideoBuffers()) { // failed to create buffers, blow everything else up.. ogg_stream_clear(&mOggTheoraStream); theora_clear(&mTheoraState); theora_comment_clear(&mTheoraComment); theora_info_clear(&mTheoraInfo); ogg_sync_clear(&mOggSyncState); // And destroy our texture. destroyTexture(); return false; } mReady = true; } if(doPlay) play(); return true; } bool TheoraTexture::parseHeaders() { ogg_packet sOggPacket; S32 nTheora = 0; S32 nVorbis = 0; S32 ret; mHasVorbis = false; // Parse the headers // find theora and vorbis streams // search pages till you find the headers mTheoraFile->setPosition(0); while(1) { ret = bufferOggPage(); if(ret == 0) break; if(!ogg_sync_pageout(&mOggSyncState, &mOggPage)) break; ogg_stream_state testStream; if(!ogg_page_bos(&mOggPage)) { // this is not an initial header, queue it up // exit stream header finding loop (headers always come before non header stuff) queueOggPage(&mOggPage); break; } // create a stream ogg_stream_init(&testStream, ogg_page_serialno(&mOggPage)); ogg_stream_pagein(&testStream, &mOggPage); ogg_stream_packetout(&testStream, &sOggPacket); // test if its a theora header if(theora_decode_header(&mTheoraInfo, &mTheoraComment, &sOggPacket) >= 0) { // it is theora, copy testStream over to the theora stream dMemcpy(&mOggTheoraStream, &testStream, sizeof(testStream)); nTheora = 1; } // test if its vorbis else if(vorbis_synthesis_headerin(&mVorbisInfo, &mVorbisComment, &sOggPacket) >= 0) { // it is vorbis, copy testStream over to the vorbis stream dMemcpy(&mOggVorbisStream, &testStream, sizeof(testStream)); mHasVorbis = true; nVorbis = 1; } else { // some other stream header? unsupported, toss it ogg_stream_clear(&testStream); } // if both vorbis and theora have been found, exit loop if(nVorbis && nTheora) break; } if(!nTheora) { // no theora stream header found Con::errorf("TheoraTexture::parseHeaders - No theora stream headers found."); // HAVE to have theora, thats what this class is for. return failure return false; } // we've now identified all the streams. parse (toss) the secondary header packets. // it looks like we just have to throw out a few packets from the header page // so that they arent mistaken as theora movie data? nothing is done with these things.. while((nTheora < 3) || (nVorbis && nVorbis < 3)) { // look for further theora headers while((nTheora < 3) && (ret = ogg_stream_packetout(&mOggTheoraStream, &sOggPacket))) { if(ret < 0) { Con::errorf("TheoraPlayer::parseHeaders - Error parsing Theora stream headers; corrupt stream? (nothing read?)"); return false; } if(theora_decode_header(&mTheoraInfo, &mTheoraComment, &sOggPacket)) { Con::errorf("TheoraPlayer::parseHeaders - Error parsing Theora stream headers; corrupt stream? (failed to decode)"); return false; } // Sanity around corrupt headers. nTheora++; if(nTheora == 3) break; } // look for more vorbis headers while((nVorbis) && (nVorbis < 3) && (ret = ogg_stream_packetout(&mOggVorbisStream, &sOggPacket))) { if(ret < 0) { Con::errorf("Error parsing vorbis stream headers; corrupt stream? (nothing read?)"); return false; } if(vorbis_synthesis_headerin(&mVorbisInfo, &mVorbisComment, &sOggPacket)) { Con::errorf("Error parsing Vorbis stream headers; corrupt stream? (bad synthesis_headerin)"); return false; } nVorbis++; if(nVorbis == 3) break; } // The header pages/packets will arrive before anything else we // care about, or the stream is not obeying spec // continue searching the next page for the headers // put the next page into the theora stream if(ogg_sync_pageout(&mOggSyncState, &mOggPage) > 0) { queueOggPage(&mOggPage); } else { // if there are no more pages, buffer another one const S32 ret = bufferOggPage(); // if there is nothing left to buffer.. if(ret == 0) { Con::errorf("TheoraTexture::parseHeaders - End of file while searching for codec headers."); return false; } } } return true; } bool TheoraTexture::createVideoBuffers() { // Set up our texture. const GBitmap *bmp = new GBitmap( getMax((U32)mTheoraInfo.frame_width, (U32)mTheoraInfo.width), getMax((U32)mTheoraInfo.frame_height, (U32)mTheoraInfo.height), false, GBitmap::RGB); mTextureHandle = new TextureHandle(NULL, bmp, true); // generate yuv conversion lookup tables generateLookupTables(); return true; } bool TheoraTexture::createAudioBuffers(Audio::Description* desc) { // Just to be sure, clear out Trevor. mMagicalTrevor.reset(); // if they didnt pass a description... if(!desc) { // ...fill a default static Audio::Description sDesc; desc = &sDesc; sDesc.mReferenceDistance = 1.0f; sDesc.mMaxDistance = 100.0f; sDesc.mConeInsideAngle = 360; sDesc.mConeOutsideAngle = 360; sDesc.mConeOutsideVolume = 1.0f; sDesc.mConeVector.set(0, 0, 1); sDesc.mEnvironmentLevel = 0.f; sDesc.mLoopCount = -1; sDesc.mMinLoopGap = 0; sDesc.mMaxLoopGap = 0; sDesc.mIs3D = false; sDesc.mVolume = 1.0f; sDesc.mIsLooping = false; sDesc.mType = 1; sDesc.mIsStreaming = true; } // create an audio handle to use mVorbisHandle = alxCreateSource(desc, "oggMixedStream"); if(!mVorbisHandle) { Con::errorf("Could not alxCreateSource for oggMixedStream.\n"); return false; } // get a pointer for it mVorbisBuffer = dynamic_cast(alxFindAudioStreamSource(mVorbisHandle)); if(!mVorbisBuffer) { alxStop(mVorbisHandle); // not sure how alxStop would find it if i couldnt.. Con::errorf("Could not find oggMixedStreamSource ptr."); return false; } return true; } // 6K!! memory needed to speed up theora player. Small price to pay! static S32 sAdjCrr[256]; static S32 sAdjCrg[256]; static S32 sAdjCbg[256]; static S32 sAdjCbb[256]; static S32 sAdjY[256]; static U8 sClampBuff[1024]; static U8* sClamp = sClampBuff + 384; // precalculate adjusted YUV values for faster RGB conversion void TheoraTexture::generateLookupTables() { static bool fGenerated = false; S32 i; for(i = 0; i < 256; i++) { sAdjCrr[i] = (409 * (i - 128) + 128) >> 8; sAdjCrg[i] = (208 * (i - 128) + 128) >> 8; sAdjCbg[i] = (100 * (i - 128) + 128) >> 8; sAdjCbb[i] = (516 * (i - 128) + 128) >> 8; sAdjY[i] = (298 * (i - 16)) >> 8; } // and setup LUT clamp range for(i = -384; i < 640; i++) { sClamp[i] = mClamp(i, 0, 0xFF); } } void TheoraTexture::drawFrame() { yuv_buffer yuv; // decode a frame! (into yuv) theora_decode_YUVout(&mTheoraState, &yuv); // get destination buffer (and 1 row offset) GBitmap *bmp = mTextureHandle->getBitmap(); U8* dst0 = bmp->getAddress(0, 0); U8 *dst1 = dst0 + bmp->getWidth() * bmp->bytesPerPixel; // find picture offset const S32 pictOffset = yuv.y_stride * mTheoraInfo.offset_y + mTheoraInfo.offset_x; const U8 *pY0, *pY1, *pU, *pV; for(S32 y = 0; y < yuv.y_height; y += 2) { // set pointers into yuv buffers (2 lines for y) pY0 = yuv.y + pictOffset + y * (yuv.y_stride); pY1 = yuv.y + pictOffset + (y | 1) * (yuv.y_stride); pU = yuv.u + ((pictOffset + y * (yuv.uv_stride)) >> 1); pV = yuv.v + ((pictOffset + y * (yuv.uv_stride)) >> 1); for(S32 x = 0; x < yuv.y_width; x += 2) { // convert a 2x2 block over // speed up G conversion a very very small amount ;) const S32 G = sAdjCrg[*pV] + sAdjCbg[*pU]; // pixel 0x0 *dst0++ = sClamp[sAdjY[*pY0] + sAdjCrr[*pV]]; *dst0++ = sClamp[sAdjY[*pY0] - G]; *dst0++ = sClamp[sAdjY[*pY0++] + sAdjCbb[*pU]]; // pixel 1x0 *dst0++ = sClamp[sAdjY[*pY0] + sAdjCrr[*pV]]; *dst0++ = sClamp[sAdjY[*pY0] - G]; *dst0++ = sClamp[sAdjY[*pY0++] + sAdjCbb[*pU]]; // pixel 0x1 *dst1++ = sClamp[sAdjY[*pY1] + sAdjCrr[*pV]]; *dst1++ = sClamp[sAdjY[*pY1] - G]; *dst1++ = sClamp[sAdjY[*pY1++] + sAdjCbb[*pU]]; // pixel 1x1 *dst1++ = sClamp[sAdjY[*pY1] + sAdjCrr[*pV++]]; *dst1++ = sClamp[sAdjY[*pY1] - G]; *dst1++ = sClamp[sAdjY[*pY1++] + sAdjCbb[*pU++]]; } // shift the destination pointers a row (loop incs 2 at a time) dst0 = dst1; dst1 += bmp->getWidth() * bmp->bytesPerPixel; } } bool TheoraTexture::play() { if(mReady && !mPlaying) mPlayThread = new Thread((ThreadRunFunction)playThread, (S32) this, 1); return mPlayThread; } void TheoraTexture::stop() { mPlaying = false; if(mPlayThread) { SAFE_DELETE(mPlayThread); } } void TheoraTexture::playThread( void *udata ) { TheoraTexture* pThis = (TheoraTexture *)udata; pThis->playLoop(); } bool TheoraTexture::playLoop() { bool fMoreVideo = true; bool fMoreAudio = mHasVorbis; // timing variables F64 dVBuffTime = 0; F64 dLastFrame = 0; mPlaying = true; mCurrentTime = 0.f; mStartTick = Platform::getRealMilliseconds(); bool isAudioActive = false; while(mPlaying) { if(fMoreAudio) fMoreAudio = readyAudio(); if(fMoreVideo) fMoreVideo = readyVideo(dLastFrame, dVBuffTime); if(!fMoreVideo && !fMoreAudio) break; // if we have no more audio to buffer, and no more video frames to display, we are done // if we haven't started yet, start it! :) if(!isAudioActive && mHasVorbis) { alxPlay(mVorbisHandle); isAudioActive = true; } // if we're set for the next frame, sleep /*S32 t = (int)((double) 1000 * (dVBuffTime - getTheoraTime())); if(t>0) Platform::sleep(t); */ U32 safety = 0; while((dVBuffTime - getTheoraTime()) >= 0.f && safety < 500) { safety++; Platform::sleep(2); } // time to draw the frame! drawFrame(); // keep track of the last frame time dLastFrame = getTheoraTime(); } if(mHasVorbis) { alxStop(mVorbisHandle); mMagicalTrevor.reset(); } mPlaying = false; mVorbisHandle = NULL; mVorbisBuffer = NULL; return false; } // ready a single frame (not a late one either) bool TheoraTexture::readyVideo(F64 dLastFrame, F64& dVBuffTime) { ogg_packet sOggPacket; while(1) { PROFILE_START(TheoraTexture_readyVideo); // get a video packet if(ogg_stream_packetout(&mOggTheoraStream, &sOggPacket) > 0) { theora_decode_packetin(&mTheoraState, &sOggPacket); dVBuffTime = theora_granule_time(&mTheoraState, mTheoraState.granulepos); // check if this frame time has not passed yet. // If the frame is late we need to decode additional // ones and keep looping, since theora at this stage // needs to decode all frames const F64 dNow = getTheoraTime(); if((dVBuffTime - dNow) >= 0.0) { PROFILE_END(); return true; // got a good frame, not late, ready to break out } else if((dNow - dLastFrame) >= 1.0) { PROFILE_END(); return true; // display at least one frame per second, regardless } // else frame is dropped (its behind), look again } else // get another page { if(!demandOggPage()) // end of file { PROFILE_END(); return false; } } //else we got a page, try and get a frame out of it PROFILE_END(); } } // buffers up as much audio as it can fit into OggMixedStream audiostream thing bool TheoraTexture::readyAudio() { #define BUFFERSIZE 8192 ogg_packet sOggPacket; ALuint bufferId = 0; S32 ret, i, count; float **pcm; // i was making buffers to fit the exact size // but the memory manager doesn't seem to be working // with multiple threads.. S16 samples[BUFFERSIZE]; // this should be large enough // If i don't have a buffer to put samples into.. while(1) { PROFILE_START(TheoraTexture_readyAudio); bufferId = mVorbisBuffer->GetAvailableBuffer(); if(!bufferId) // buffered all that it cant fit { PROFILE_END(); return true; } // Skip if we're ahead of the video... // if the buffer is ready now // fill it! while(!(ret = vorbis_synthesis_pcmout(&mVorbisDspState, &pcm))) { // no pending audio; is there a pending packet to decode? if(ogg_stream_packetout(&mOggVorbisStream, &sOggPacket) > 0) { if(vorbis_synthesis(&mVorbisBlock, &sOggPacket) == 0) // test for success! vorbis_synthesis_blockin(&mVorbisDspState, &mVorbisBlock); } else // we need more data; break out to suck in another page { if(!demandOggPage()) { PROFILE_END(); return false; // end of file } } } // found samples to buffer! for(count = i = 0; i < ret && i < (BUFFERSIZE * mVorbisInfo.channels); i++) { for(int j = 0; j < mVorbisInfo.channels; j++) { int val = (int)(pcm[j][i] * 32767.f); if(val > 32767) val = 32767; if(val < -32768) val = -32768; #if defined(TORQUE_OS_MAC) && !defined(TORQUE_BIG_ENDIAN) samples[count++] = ((val << 8) & 0xFF00) | ((val >> 8) & 0x00FF); #else samples[count++] = val; #endif } } // bump up my buffering position vorbis_synthesis_read(&mVorbisDspState, i); // by this point the buffer should be filled (or as close as its gonna get) // ... Queue buffer alBufferData( bufferId, (mVorbisInfo.channels == 1) ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16, samples, count * 2, mVorbisInfo.rate); // Note the time for synchronization by magical trevor. const F64 newTimeSlice = F64(ret) / F64(mVorbisInfo.rate); mMagicalTrevor.postBuffer(bufferId, newTimeSlice); mVorbisBuffer->QueueBuffer(bufferId); PROFILE_END(); } } F64 TheoraTexture::getTheoraTime() { if(mHasVorbis) { // We have audio, so synch to audio track. ALint buf=-1; alGetSourcei(mVorbisBuffer->mSource, AL_BUFFER, &buf); mCurrentTime += mMagicalTrevor.advanceTime(buf); return mCurrentTime; } else { // We have no audio, just synch to start time. return (double)0.001 * (double)(Platform::getRealMilliseconds() - mStartTick); } } // helpers // function does whatever it can get pages into streams bool TheoraTexture::demandOggPage() { while(1) { if(ogg_sync_pageout(&mOggSyncState,&mOggPage) > 0) { // found a page, queue it to its stream queueOggPage(&mOggPage); return true; } if(bufferOggPage() == 0) { // Ogg buffering stopped, end of file reached return false; } } } // grabs some more compressed bitstream and syncs it for page extraction S32 TheoraTexture::bufferOggPage() { char *buffer = ogg_sync_buffer(&mOggSyncState, 4096); // this is a bit of a hack, i guess, i dont know how else to do it // you dont want to send extra data to ogg_sync or it will try and // pull it out like its real data. thats no good // grab current position U32 bytes = mTheoraFile->getPosition(); mTheoraFile->read(4096, buffer); // find out how much was read bytes = mTheoraFile->getPosition() - bytes; // give it to ogg and tell it how many bytes ogg_sync_wrote(&mOggSyncState, bytes); return bytes; } // try and put the page into the theora and vorbis streams, // they wont accept pages that arent for them S32 TheoraTexture::queueOggPage(ogg_page *page) { ogg_stream_pagein(&mOggTheoraStream, page); if(mHasVorbis) ogg_stream_pagein(&mOggVorbisStream, page); return 0; } // returns a handle for OpenGL calls U32 TheoraTexture::getGLName() { if(mTextureHandle) { mTextureHandle->refresh(); return mTextureHandle->getGLName(); } return 0; } // copies the newest texture data to openGL video memory void TheoraTexture::refresh() { if(mTextureHandle) mTextureHandle->refresh(); }