mirror of
https://github.com/scummvm/scummvm.git
synced 2026-05-21 05:40:43 +00:00
6469df1baa
A fix was introduced in 43c5d3e7c9, which
adds handling for dropped frames in theora decoding. However, this
didn't handle the case where the granule position for the current packet
is invalid.
These changes introduce a check on the granule position of the current
packet. If the granule position is valid, proceed to properly calculate
the frame number and the next frame start time from the granule
position. If the granule position is not valid, use best estimation of
for these values.
These changes also refactor to combine the checks for the two cases
where the granule position is passed to theora functions. The
documentation for both of these functions states that they will return
-1 in the case that the provided granule position is negative.
547 lines
16 KiB
C++
547 lines
16 KiB
C++
/* 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 3 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, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
/*
|
|
* Source is based on the player example from libvorbis package,
|
|
* available at: https://gitlab.xiph.org/xiph/theora/-/blob/main/examples/player_example.c
|
|
*
|
|
* THIS FILE IS PART OF THE OggTheora SOFTWARE CODEC SOURCE CODE.
|
|
* USE, DISTRIBUTION AND REPRODUCTION OF THIS LIBRARY SOURCE IS
|
|
* GOVERNED BY A BSD-STYLE SOURCE LICENSE INCLUDED WITH THIS SOURCE
|
|
* IN 'COPYING'. PLEASE READ THESE TERMS BEFORE DISTRIBUTING.
|
|
*
|
|
* THE Theora SOURCE CODE IS COPYRIGHT (C) 2002-2009
|
|
* by the Xiph.Org Foundation and contributors http://www.xiph.org/
|
|
*
|
|
*/
|
|
|
|
#include "video/theora_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"
|
|
#include "image/codecs/codec.h"
|
|
|
|
namespace Video {
|
|
|
|
TheoraDecoder::TheoraDecoder() {
|
|
_fileStream = 0;
|
|
|
|
_videoTrack = 0;
|
|
_audioTrack = 0;
|
|
_hasVideo = _hasAudio = false;
|
|
}
|
|
|
|
TheoraDecoder::~TheoraDecoder() {
|
|
close();
|
|
}
|
|
|
|
bool TheoraDecoder::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 TheoraVideoTrack(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 TheoraDecoder::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 TheoraDecoder::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();
|
|
}
|
|
|
|
Common::Rational TheoraDecoder::getFrameRate() const {
|
|
if (_videoTrack)
|
|
return _videoTrack->getFrameRate();
|
|
return Common::Rational();
|
|
}
|
|
|
|
TheoraDecoder::TheoraVideoTrack::TheoraVideoTrack(th_info &theoraInfo, th_setup_info *theoraSetup) {
|
|
_theoraDecode = th_decode_alloc(&theoraInfo, theoraSetup);
|
|
|
|
if (theoraInfo.pixel_fmt != TH_PF_420 && theoraInfo.pixel_fmt != TH_PF_422 && theoraInfo.pixel_fmt != TH_PF_444) {
|
|
error("Found unknown Theora format (must be YUV420, YUV422 or YUV444)");
|
|
}
|
|
|
|
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));
|
|
|
|
_x = theoraInfo.pic_x;
|
|
_y = theoraInfo.pic_y;
|
|
_width = theoraInfo.pic_width;
|
|
_height = theoraInfo.pic_height;
|
|
_surfaceWidth = theoraInfo.frame_width;
|
|
_surfaceHeight = theoraInfo.frame_height;
|
|
|
|
_pixelFormat = Image::Codec::getDefaultYUVFormat();
|
|
_theoraPixelFormat = theoraInfo.pixel_fmt;
|
|
|
|
// Set the frame rate
|
|
_frameRate = Common::Rational(theoraInfo.fps_numerator, theoraInfo.fps_denominator);
|
|
|
|
_endOfVideo = false;
|
|
_nextFrameStartTime = 0.0;
|
|
_curFrame = -1;
|
|
_surface = nullptr;
|
|
_displaySurface = nullptr;
|
|
}
|
|
|
|
TheoraDecoder::TheoraVideoTrack::~TheoraVideoTrack() {
|
|
th_decode_free(_theoraDecode);
|
|
|
|
if (_surface) {
|
|
_surface->free();
|
|
delete _surface;
|
|
_surface = nullptr;
|
|
}
|
|
|
|
if (_displaySurface) {
|
|
_displaySurface->setPixels(0);
|
|
delete _displaySurface;
|
|
_displaySurface = nullptr;
|
|
}
|
|
}
|
|
|
|
bool TheoraDecoder::TheoraVideoTrack::decodePacket(ogg_packet &oggPacket) {
|
|
int decodeRes = th_decode_packetin(_theoraDecode, &oggPacket, 0);
|
|
|
|
bool gotNewFrame = decodeRes == 0; // new frame, decoding needed
|
|
bool gotDupFrame = decodeRes == TH_DUPFRAME; // no decoding needed, just update timing
|
|
|
|
if (gotNewFrame || gotDupFrame) {
|
|
if (gotNewFrame) {
|
|
// Convert YUV data to RGB data
|
|
th_ycbcr_buffer yuv;
|
|
th_decode_ycbcr_out(_theoraDecode, yuv);
|
|
translateYUVtoRGBA(yuv);
|
|
}
|
|
|
|
// If we have a valid granule position for this packet, use it to calculate the next
|
|
// frame information. If we don't have a valid granule position, we need to do our
|
|
// calculation for the frame number and timing.
|
|
if (oggPacket.granulepos >= 0) {
|
|
_curFrame = (int)th_granule_frame(_theoraDecode, oggPacket.granulepos);
|
|
_nextFrameStartTime = th_granule_time(_theoraDecode, oggPacket.granulepos);
|
|
} else {
|
|
_curFrame++;
|
|
_nextFrameStartTime += _frameRate.getInverse().toDouble();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
enum TheoraYUVBuffers {
|
|
kBufferY = 0,
|
|
kBufferU = 1,
|
|
kBufferV = 2
|
|
};
|
|
|
|
void TheoraDecoder::TheoraVideoTrack::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 components must be half or equal the Y component
|
|
assert((YUVBuffer[kBufferU].width == YUVBuffer[kBufferY].width >> 1) || (YUVBuffer[kBufferU].width == YUVBuffer[kBufferY].width));
|
|
assert((YUVBuffer[kBufferV].width == YUVBuffer[kBufferY].width >> 1) || (YUVBuffer[kBufferV].width == YUVBuffer[kBufferY].width));
|
|
assert((YUVBuffer[kBufferU].height == YUVBuffer[kBufferY].height >> 1) || (YUVBuffer[kBufferU].height == YUVBuffer[kBufferY].height));
|
|
assert((YUVBuffer[kBufferV].height == YUVBuffer[kBufferY].height >> 1) || (YUVBuffer[kBufferV].height == YUVBuffer[kBufferY].height));
|
|
|
|
if (!_surface) {
|
|
_surface = new Graphics::Surface();
|
|
_surface->create(_surfaceWidth, _surfaceHeight, _pixelFormat);
|
|
}
|
|
|
|
// Set up a display surface
|
|
if (!_displaySurface) {
|
|
_displaySurface = new Graphics::Surface();
|
|
_displaySurface->init(_width, _height, _surface->pitch,
|
|
_surface->getBasePtr(_x, _y), _surface->format);
|
|
}
|
|
|
|
switch (_theoraPixelFormat) {
|
|
case TH_PF_420:
|
|
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);
|
|
break;
|
|
case TH_PF_422:
|
|
YUVToRGBMan.convert422(_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);
|
|
break;
|
|
case TH_PF_444:
|
|
YUVToRGBMan.convert444(_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);
|
|
break;
|
|
default:
|
|
error("Unsupported Theora pixel format");
|
|
}
|
|
}
|
|
|
|
static vorbis_info *info = 0;
|
|
|
|
TheoraDecoder::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;
|
|
}
|
|
|
|
TheoraDecoder::VorbisAudioTrack::~VorbisAudioTrack() {
|
|
vorbis_dsp_clear(&_vorbisDSP);
|
|
vorbis_block_clear(&_vorbisBlock);
|
|
delete _audStream;
|
|
free(_audioBuffer);
|
|
}
|
|
|
|
Audio::AudioStream *TheoraDecoder::VorbisAudioTrack::getAudioStream() const {
|
|
return _audStream;
|
|
}
|
|
|
|
#define AUDIOFD_FRAGSIZE 10240
|
|
|
|
#ifndef USE_TREMOR
|
|
static double rint(double v) {
|
|
return floor(v + 0.5);
|
|
}
|
|
#endif
|
|
|
|
bool TheoraDecoder::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 TheoraDecoder::VorbisAudioTrack::hasAudio() const {
|
|
return _audStream->numQueuedStreams() > 0;
|
|
}
|
|
|
|
bool TheoraDecoder::VorbisAudioTrack::needsAudio() const {
|
|
// TODO: 5 is very arbitrary. We probably should do something like QuickTime does.
|
|
return !_endOfAudio && _audStream->numQueuedStreams() < 5;
|
|
}
|
|
|
|
void TheoraDecoder::VorbisAudioTrack::synthesizePacket(ogg_packet &oggPacket) {
|
|
if (vorbis_synthesis(&_vorbisBlock, &oggPacket) == 0) // test for success
|
|
vorbis_synthesis_blockin(&_vorbisDSP, &_vorbisBlock);
|
|
}
|
|
|
|
void TheoraDecoder::queuePage(ogg_page *page) {
|
|
if (_hasVideo)
|
|
ogg_stream_pagein(&_theoraOut, page);
|
|
|
|
if (_hasAudio)
|
|
ogg_stream_pagein(&_vorbisOut, page);
|
|
}
|
|
|
|
int TheoraDecoder::bufferData() {
|
|
char *buffer = ogg_sync_buffer(&_oggSync, 4096);
|
|
int bytes = _fileStream->read(buffer, 4096);
|
|
|
|
ogg_sync_wrote(&_oggSync, bytes);
|
|
|
|
return bytes;
|
|
}
|
|
|
|
bool TheoraDecoder::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 TheoraDecoder::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
|