diff --git a/video/mkv_decoder.cpp b/video/mkv_decoder.cpp new file mode 100644 index 00000000000..a09e69d605f --- /dev/null +++ b/video/mkv_decoder.cpp @@ -0,0 +1,481 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "video/mkv_decoder.h" + +#include "audio/audiostream.h" +#include "audio/decoders/raw.h" +#include "common/stream.h" +#include "common/system.h" +#include "common/textconsole.h" +#include "common/util.h" +#include "graphics/pixelformat.h" +#include "graphics/yuv_to_rgb.h" + +namespace Video { + +MKVDecoder::MKVDecoder() { + _fileStream = 0; + + _videoTrack = 0; + _audioTrack = 0; + _hasVideo = _hasAudio = false; +} + +MKVDecoder::~MKVDecoder() { + close(); +} + +bool MKVDecoder::loadStream(Common::SeekableReadStream *stream) { + close(); + + _fileStream = stream; + + // start up Ogg stream synchronization layer + ogg_sync_init(&_oggSync); + + // init supporting Vorbis structures needed in header parsing + vorbis_info_init(&_vorbisInfo); + vorbis_comment vorbisComment; + vorbis_comment_init(&vorbisComment); + + // init supporting Theora structures needed in header parsing + th_info theoraInfo; + th_info_init(&theoraInfo); + th_comment theoraComment; + th_comment_init(&theoraComment); + th_setup_info *theoraSetup = 0; + + uint theoraPackets = 0, vorbisPackets = 0; + + // Ogg file open; parse the headers + // Only interested in Vorbis/Theora streams + bool foundHeader = false; + while (!foundHeader) { + int ret = bufferData(); + + if (ret == 0) + break; // FIXME: Shouldn't this error out? + + while (ogg_sync_pageout(&_oggSync, &_oggPage) > 0) { + ogg_stream_state test; + + // is this a mandated initial header? If not, stop parsing + if (!ogg_page_bos(&_oggPage)) { + // don't leak the page; get it into the appropriate stream + queuePage(&_oggPage); + foundHeader = true; + break; + } + + ogg_stream_init(&test, ogg_page_serialno(&_oggPage)); + ogg_stream_pagein(&test, &_oggPage); + ogg_stream_packetout(&test, &_oggPacket); + + // identify the codec: try theora + if (theoraPackets == 0 && th_decode_headerin(&theoraInfo, &theoraComment, &theoraSetup, &_oggPacket) >= 0) { + // it is theora + memcpy(&_theoraOut, &test, sizeof(test)); + theoraPackets = 1; + _hasVideo = true; + } else if (vorbisPackets == 0 && vorbis_synthesis_headerin(&_vorbisInfo, &vorbisComment, &_oggPacket) >= 0) { + // it is vorbis + memcpy(&_vorbisOut, &test, sizeof(test)); + vorbisPackets = 1; + _hasAudio = true; + } else { + // whatever it is, we don't care about it + ogg_stream_clear(&test); + } + } + // fall through to non-bos page parsing + } + + // we're expecting more header packets. + while ((theoraPackets && theoraPackets < 3) || (vorbisPackets && vorbisPackets < 3)) { + int ret; + + // look for further theora headers + while (theoraPackets && (theoraPackets < 3) && (ret = ogg_stream_packetout(&_theoraOut, &_oggPacket))) { + if (ret < 0) + error("Error parsing Theora stream headers; corrupt stream?"); + + if (!th_decode_headerin(&theoraInfo, &theoraComment, &theoraSetup, &_oggPacket)) + error("Error parsing Theora stream headers; corrupt stream?"); + + theoraPackets++; + } + + // look for more vorbis header packets + while (vorbisPackets && (vorbisPackets < 3) && (ret = ogg_stream_packetout(&_vorbisOut, &_oggPacket))) { + if (ret < 0) + error("Error parsing Vorbis stream headers; corrupt stream?"); + + if (vorbis_synthesis_headerin(&_vorbisInfo, &vorbisComment, &_oggPacket)) + error("Error parsing Vorbis stream headers; corrupt stream?"); + + vorbisPackets++; + + if (vorbisPackets == 3) + break; + } + + // The header pages/packets will arrive before anything else we + // care about, or the stream is not obeying spec + + if (ogg_sync_pageout(&_oggSync, &_oggPage) > 0) { + queuePage(&_oggPage); // demux into the appropriate stream + } else { + ret = bufferData(); // someone needs more data + + if (ret == 0) + error("End of file while searching for codec headers."); + } + } + + // And now we have it all. Initialize decoders next + if (_hasVideo) { + _videoTrack = new VPXVideoTrack(getDefaultHighColorFormat(), theoraInfo, theoraSetup); + addTrack(_videoTrack); + } + + th_info_clear(&theoraInfo); + th_comment_clear(&theoraComment); + th_setup_free(theoraSetup); + + if (_hasAudio) { + _audioTrack = new VorbisAudioTrack(getSoundType(), _vorbisInfo); + + // Get enough audio data to start us off + while (!_audioTrack->hasAudio()) { + // Queue more data + bufferData(); + while (ogg_sync_pageout(&_oggSync, &_oggPage) > 0) + queuePage(&_oggPage); + + queueAudio(); + } + + addTrack(_audioTrack); + } + + vorbis_comment_clear(&vorbisComment); + + return true; +} + +void MKVDecoder::close() { + VideoDecoder::close(); + + if (!_fileStream) + return; + + if (_videoTrack) { + ogg_stream_clear(&_theoraOut); + _videoTrack = 0; + } + + if (_audioTrack) { + ogg_stream_clear(&_vorbisOut); + _audioTrack = 0; + } + + ogg_sync_clear(&_oggSync); + vorbis_info_clear(&_vorbisInfo); + + delete _fileStream; + _fileStream = 0; + + _hasVideo = _hasAudio = false; +} + +void MKVDecoder::readNextPacket() { + // First, let's get our frame + if (_hasVideo) { + while (!_videoTrack->endOfTrack()) { + // theora is one in, one out... + if (ogg_stream_packetout(&_theoraOut, &_oggPacket) > 0) { + if (_videoTrack->decodePacket(_oggPacket)) + break; + } else if (_theoraOut.e_o_s || _fileStream->eos()) { + // If we can't get any more frames, we're done. + _videoTrack->setEndOfVideo(); + } else { + // Queue more data + bufferData(); + while (ogg_sync_pageout(&_oggSync, &_oggPage) > 0) + queuePage(&_oggPage); + } + + // Update audio if we can + queueAudio(); + } + } + + // Then make sure we have enough audio buffered + ensureAudioBufferSize(); +} + +MKVDecoder::VPXVideoTrack::VPXVideoTrack(const Graphics::PixelFormat &format, th_info &theoraInfo, th_setup_info *theoraSetup) { + _theoraDecode = th_decode_alloc(&theoraInfo, theoraSetup); + + if (theoraInfo.pixel_fmt != TH_PF_420) + error("Only theora YUV420 is supported"); + + int postProcessingMax; + th_decode_ctl(_theoraDecode, TH_DECCTL_GET_PPLEVEL_MAX, &postProcessingMax, sizeof(postProcessingMax)); + th_decode_ctl(_theoraDecode, TH_DECCTL_SET_PPLEVEL, &postProcessingMax, sizeof(postProcessingMax)); + + _surface.create(theoraInfo.frame_width, theoraInfo.frame_height, format); + + // Set up a display surface + _displaySurface.init(theoraInfo.pic_width, theoraInfo.pic_height, _surface.pitch, + _surface.getBasePtr(theoraInfo.pic_x, theoraInfo.pic_y), format); + + // Set the frame rate + _frameRate = Common::Rational(theoraInfo.fps_numerator, theoraInfo.fps_denominator); + + _endOfVideo = false; + _nextFrameStartTime = 0.0; + _curFrame = -1; +} + +MKVDecoder::VPXVideoTrack::~VPXVideoTrack() { + th_decode_free(_theoraDecode); + + _surface.free(); + _displaySurface.setPixels(0); +} + +bool MKVDecoder::VPXVideoTrack::decodePacket(ogg_packet &oggPacket) { + if (th_decode_packetin(_theoraDecode, &oggPacket, 0) == 0) { + _curFrame++; + + // Convert YUV data to RGB data + th_ycbcr_buffer yuv; + th_decode_ycbcr_out(_theoraDecode, yuv); + translateYUVtoRGBA(yuv); + + double time = th_granule_time(_theoraDecode, oggPacket.granulepos); + + // We need to calculate when the next frame should be shown + // This is all in floating point because that's what the Ogg code gives us + // Ogg is a lossy container format, so it doesn't always list the time to the + // next frame. In such cases, we need to calculate it ourselves. + if (time == -1.0) + _nextFrameStartTime += _frameRate.getInverse().toDouble(); + else + _nextFrameStartTime = time; + + return true; + } + + return false; +} + +enum TheoraYUVBuffers { + kBufferY = 0, + kBufferU = 1, + kBufferV = 2 +}; + +void MKVDecoder::VPXVideoTrack::translateYUVtoRGBA(th_ycbcr_buffer &YUVBuffer) { + // Width and height of all buffers have to be divisible by 2. + assert((YUVBuffer[kBufferY].width & 1) == 0); + assert((YUVBuffer[kBufferY].height & 1) == 0); + assert((YUVBuffer[kBufferU].width & 1) == 0); + assert((YUVBuffer[kBufferV].width & 1) == 0); + + // UV images have to have a quarter of the Y image resolution + assert(YUVBuffer[kBufferU].width == YUVBuffer[kBufferY].width >> 1); + assert(YUVBuffer[kBufferV].width == YUVBuffer[kBufferY].width >> 1); + assert(YUVBuffer[kBufferU].height == YUVBuffer[kBufferY].height >> 1); + assert(YUVBuffer[kBufferV].height == YUVBuffer[kBufferY].height >> 1); + + YUVToRGBMan.convert420(&_surface, Graphics::YUVToRGBManager::kScaleITU, YUVBuffer[kBufferY].data, YUVBuffer[kBufferU].data, YUVBuffer[kBufferV].data, YUVBuffer[kBufferY].width, YUVBuffer[kBufferY].height, YUVBuffer[kBufferY].stride, YUVBuffer[kBufferU].stride); +} + +static vorbis_info *info = 0; + +MKVDecoder::VorbisAudioTrack::VorbisAudioTrack(Audio::Mixer::SoundType soundType, vorbis_info &vorbisInfo) : + AudioTrack(soundType) { + vorbis_synthesis_init(&_vorbisDSP, &vorbisInfo); + vorbis_block_init(&_vorbisDSP, &_vorbisBlock); + info = &vorbisInfo; + + _audStream = Audio::makeQueuingAudioStream(vorbisInfo.rate, vorbisInfo.channels != 1); + + _audioBufferFill = 0; + _audioBuffer = 0; + _endOfAudio = false; +} + +MKVDecoder::VorbisAudioTrack::~VorbisAudioTrack() { + vorbis_dsp_clear(&_vorbisDSP); + vorbis_block_clear(&_vorbisBlock); + delete _audStream; + free(_audioBuffer); +} + +Audio::AudioStream *MKVDecoder::VorbisAudioTrack::getAudioStream() const { + return _audStream; +} + +#define AUDIOFD_FRAGSIZE 10240 + +#ifndef USE_TREMOR +static double rint(double v) { + return floor(v + 0.5); +} +#endif + +bool MKVDecoder::VorbisAudioTrack::decodeSamples() { +#ifdef USE_TREMOR + ogg_int32_t **pcm; +#else + float **pcm; +#endif + + // if there's pending, decoded audio, grab it + int ret = vorbis_synthesis_pcmout(&_vorbisDSP, &pcm); + + if (ret > 0) { + if (!_audioBuffer) { + _audioBuffer = (ogg_int16_t *)malloc(AUDIOFD_FRAGSIZE * sizeof(ogg_int16_t)); + assert(_audioBuffer); + } + + int channels = _audStream->isStereo() ? 2 : 1; + int count = _audioBufferFill / 2; + int maxsamples = ((AUDIOFD_FRAGSIZE - _audioBufferFill) / channels) >> 1; + int i; + + for (i = 0; i < ret && i < maxsamples; i++) { + for (int j = 0; j < channels; j++) { +#ifdef USE_TREMOR + int val = CLIP((int)pcm[j][i] >> 9, -32768, 32767); +#else + int val = CLIP((int)rint(pcm[j][i] * 32767.f), -32768, 32767); +#endif + _audioBuffer[count++] = val; + } + } + + vorbis_synthesis_read(&_vorbisDSP, i); + _audioBufferFill += (i * channels) << 1; + + if (_audioBufferFill == AUDIOFD_FRAGSIZE) { + byte flags = Audio::FLAG_16BITS; + + if (_audStream->isStereo()) + flags |= Audio::FLAG_STEREO; + +#ifdef SCUMM_LITTLE_ENDIAN + flags |= Audio::FLAG_LITTLE_ENDIAN; +#endif + + _audStream->queueBuffer((byte *)_audioBuffer, AUDIOFD_FRAGSIZE, DisposeAfterUse::YES, flags); + + // The audio mixer is now responsible for the old audio buffer. + // We need to create a new one. + _audioBuffer = 0; + _audioBufferFill = 0; + } + + return true; + } + + return false; +} + +bool MKVDecoder::VorbisAudioTrack::hasAudio() const { + return _audStream->numQueuedStreams() > 0; +} + +bool MKVDecoder::VorbisAudioTrack::needsAudio() const { + // TODO: 5 is very arbitrary. We probably should do something like QuickTime does. + return !_endOfAudio && _audStream->numQueuedStreams() < 5; +} + +void MKVDecoder::VorbisAudioTrack::synthesizePacket(ogg_packet &oggPacket) { + if (vorbis_synthesis(&_vorbisBlock, &oggPacket) == 0) // test for success + vorbis_synthesis_blockin(&_vorbisDSP, &_vorbisBlock); +} + +void MKVDecoder::queuePage(ogg_page *page) { + if (_hasVideo) + ogg_stream_pagein(&_theoraOut, page); + + if (_hasAudio) + ogg_stream_pagein(&_vorbisOut, page); +} + +int MKVDecoder::bufferData() { + char *buffer = ogg_sync_buffer(&_oggSync, 4096); + int bytes = _fileStream->read(buffer, 4096); + + ogg_sync_wrote(&_oggSync, bytes); + + return bytes; +} + +bool MKVDecoder::queueAudio() { + if (!_hasAudio) + return false; + + bool queuedAudio = false; + + for (;;) { + if (_audioTrack->decodeSamples()) { + // we queued some pending audio + queuedAudio = true; + } else if (ogg_stream_packetout(&_vorbisOut, &_oggPacket) > 0) { + // no pending audio; is there a pending packet to decode? + _audioTrack->synthesizePacket(_oggPacket); + } else { + // we've buffered all we have, break out for now + break; + } + } + + return queuedAudio; +} + +void MKVDecoder::ensureAudioBufferSize() { + if (!_hasAudio) + return; + + // Force at least some audio to be buffered + while (_audioTrack->needsAudio()) { + bufferData(); + while (ogg_sync_pageout(&_oggSync, &_oggPage) > 0) + queuePage(&_oggPage); + + bool queuedAudio = queueAudio(); + if ((_vorbisOut.e_o_s || _fileStream->eos()) && !queuedAudio) { + _audioTrack->setEndOfAudio(); + break; + } + } +} + +} // End of namespace Video diff --git a/video/mkv_decoder.h b/video/mkv_decoder.h new file mode 100644 index 00000000000..3f4329054e4 --- /dev/null +++ b/video/mkv_decoder.h @@ -0,0 +1,157 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "common/scummsys.h" // for USE_VPX + +#ifdef USE_VPX + +#ifndef VIDEO_MKV_DECODER_H +#define VIDEO_MKV_DECODER_H + +#include "common/rational.h" +#include "video/video_decoder.h" +#include "audio/mixer.h" +#include "graphics/surface.h" + +#include + +#ifdef USE_TREMOR +#include +#else +#include +#endif + +namespace Common { +class SeekableReadStream; +} + +namespace Audio { +class AudioStream; +class QueuingAudioStream; +} + +namespace Video { + +/** + * + * Decoder for MKV/Webm videos. + * Video decoder used in engines: + * - sludge + */ +class MKVDecoder : public VideoDecoder { +public: + MKVDecoder(); + virtual ~MKVDecoder(); + + /** + * Load a video file + * @param stream the stream to load + */ + bool loadStream(Common::SeekableReadStream *stream); + void close(); + +protected: + void readNextPacket(); + +private: + class VPXVideoTrack : public VideoTrack { + public: + VPXVideoTrack(const Graphics::PixelFormat &format, th_info &theoraInfo, th_setup_info *theoraSetup); + ~VPXVideoTrack(); + + bool endOfTrack() const { return _endOfVideo; } + uint16 getWidth() const { return _displaySurface.w; } + uint16 getHeight() const { return _displaySurface.h; } + Graphics::PixelFormat getPixelFormat() const { return _displaySurface.format; } + int getCurFrame() const { return _curFrame; } + uint32 getNextFrameStartTime() const { return (uint32)(_nextFrameStartTime * 1000); } + const Graphics::Surface *decodeNextFrame() { return &_displaySurface; } + + bool decodePacket(ogg_packet &oggPacket); + void setEndOfVideo() { _endOfVideo = true; } + + private: + int _curFrame; + bool _endOfVideo; + Common::Rational _frameRate; + double _nextFrameStartTime; + + Graphics::Surface _surface; + Graphics::Surface _displaySurface; + + th_dec_ctx *_theoraDecode; + + void translateYUVtoRGBA(th_ycbcr_buffer &YUVBuffer); + }; + + class VorbisAudioTrack : public AudioTrack { + public: + VorbisAudioTrack(Audio::Mixer::SoundType soundType, vorbis_info &vorbisInfo); + ~VorbisAudioTrack(); + + bool decodeSamples(); + bool hasAudio() const; + bool needsAudio() const; + void synthesizePacket(ogg_packet &oggPacket); + void setEndOfAudio() { _endOfAudio = true; } + + protected: + Audio::AudioStream *getAudioStream() const; + + private: + // single audio fragment audio buffering + int _audioBufferFill; + ogg_int16_t *_audioBuffer; + + Audio::QueuingAudioStream *_audStream; + + vorbis_block _vorbisBlock; + vorbis_dsp_state _vorbisDSP; + + bool _endOfAudio; + }; + + void queuePage(ogg_page *page); + int bufferData(); + bool queueAudio(); + void ensureAudioBufferSize(); + + Common::SeekableReadStream *_fileStream; + + ogg_sync_state _oggSync; + ogg_page _oggPage; + ogg_packet _oggPacket; + + ogg_stream_state _theoraOut, _vorbisOut; + bool _hasVideo, _hasAudio; + + vorbis_info _vorbisInfo; + + VPXVideoTrack *_videoTrack; + VorbisAudioTrack *_audioTrack; +}; + +} // End of namespace Video + +#endif + +#endif diff --git a/video/module.mk b/video/module.mk index 1ee711268f6..d73f9af0eb1 100644 --- a/video/module.mk +++ b/video/module.mk @@ -14,9 +14,7 @@ MODULE_OBJS := \ qt_decoder.o \ smk_decoder.o \ subtitles.o \ - video_decoder.o \ - mkv/mkvparser.o \ - mkv/mkvreader.o + video_decoder.o ifdef USE_BINK MODULE_OBJS += \ @@ -28,5 +26,12 @@ MODULE_OBJS += \ theora_decoder.o endif +ifdef USE_VPX +MODULE_OBJS += \ + mkv_decoder.o \ + mkv/mkvparser.o \ + mkv/mkvreader.o +endif + # Include common rules include $(srcdir)/rules.mk