Show More
Commit Description:
Various UI improvements.
Commit Description:
Various UI improvements.
References:
File last commit:
Show/Diff file:
Action:
FNA/lib/Theorafile/theorafile.c
596 lines | 11.8 KiB | text/x-c | CLexer
596 lines | 11.8 KiB | text/x-c | CLexer
r0 | /* Theorafile - Ogg Theora Video Decoder Library | |||
* | ||||
* Copyright (c) 2017-2020 Ethan Lee. | ||||
* Based on TheoraPlay, Copyright (c) 2011-2016 Ryan C. Gordon. | ||||
* | ||||
* 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. | ||||
* | ||||
* Ethan "flibitijibibo" Lee <flibitijibibo@flibitijibibo.com> | ||||
* | ||||
*/ | ||||
#include "theorafile.h" | ||||
#include <stdio.h> /* fopen and friends */ | ||||
#include <string.h> /* memcpy, memset */ | ||||
#define TF_DEFAULT_BUFFER_SIZE 4096 | ||||
#ifdef _WIN32 | ||||
#define inline __inline | ||||
#endif /* _WIN32 */ | ||||
static inline int INTERNAL_readOggData(OggTheora_File *file) | ||||
{ | ||||
long buflen = TF_DEFAULT_BUFFER_SIZE; | ||||
char *buffer = ogg_sync_buffer(&file->sync, buflen); | ||||
if (buffer == NULL) | ||||
{ | ||||
/* If you made it here, you ran out of RAM (wait, what?) */ | ||||
return -1; | ||||
} | ||||
buflen = file->io.read_func(buffer, 1, buflen, file->datasource); | ||||
if (buflen <= 0) | ||||
{ | ||||
return 0; | ||||
} | ||||
return (ogg_sync_wrote(&file->sync, buflen) == 0) ? 1 : -1; | ||||
} | ||||
static inline void INTERNAL_queueOggPage(OggTheora_File *file) | ||||
{ | ||||
if (file->tpackets) | ||||
{ | ||||
ogg_stream_pagein(&file->tstream, &file->page); | ||||
} | ||||
if (file->vpackets) | ||||
{ | ||||
ogg_stream_pagein(&file->vstream, &file->page); | ||||
} | ||||
} | ||||
static inline int INTERNAL_getNextPacket( | ||||
OggTheora_File *file, | ||||
ogg_stream_state *stream, | ||||
ogg_packet *packet | ||||
) { | ||||
while (ogg_stream_packetout(stream, packet) <= 0) | ||||
{ | ||||
const int rc = INTERNAL_readOggData(file); | ||||
if (rc == 0) | ||||
{ | ||||
file->eos = 1; | ||||
return 0; | ||||
} | ||||
else if (rc < 0) | ||||
{ | ||||
/* If you made it here, something REALLY bad happened. | ||||
* | ||||
* Unfortunately, ogg_sync_wrote does not give out any | ||||
* codes, so I have no idea what that something is. | ||||
* | ||||
* Be sure you're not doing something nasty like | ||||
* accessing one file via multiple threads at one time. | ||||
* -flibit | ||||
*/ | ||||
file->eos = 1; | ||||
return 0; | ||||
} | ||||
else | ||||
{ | ||||
while (ogg_sync_pageout(&file->sync, &file->page) > 0) | ||||
{ | ||||
INTERNAL_queueOggPage(file); | ||||
} | ||||
} | ||||
} | ||||
return 1; | ||||
} | ||||
int tf_open_callbacks(void *datasource, OggTheora_File *file, tf_callbacks io) | ||||
{ | ||||
ogg_packet packet; | ||||
ogg_stream_state filler; | ||||
th_setup_info *tsetup = NULL; | ||||
int pp_level_max = 0; | ||||
int errcode = TF_EUNKNOWN; | ||||
if (datasource == NULL) | ||||
{ | ||||
return TF_ENODATASOURCE; | ||||
} | ||||
memset(file, '\0', sizeof(OggTheora_File)); | ||||
file->datasource = datasource; | ||||
file->io = io; | ||||
#define TF_OPEN_ASSERT(cond) \ | ||||
if (cond) goto fail; | ||||
ogg_sync_init(&file->sync); | ||||
vorbis_info_init(&file->vinfo); | ||||
vorbis_comment_init(&file->vcomment); | ||||
th_info_init(&file->tinfo); | ||||
th_comment_init(&file->tcomment); | ||||
/* Is there even data for us to read...? */ | ||||
TF_OPEN_ASSERT(INTERNAL_readOggData(file) <= 0) | ||||
/* Read header */ | ||||
while (ogg_sync_pageout(&file->sync, &file->page) > 0) | ||||
{ | ||||
if (!ogg_page_bos(&file->page)) | ||||
{ | ||||
/* Not a header! */ | ||||
INTERNAL_queueOggPage(file); | ||||
break; | ||||
} | ||||
ogg_stream_init(&filler, ogg_page_serialno(&file->page)); | ||||
ogg_stream_pagein(&filler, &file->page); | ||||
ogg_stream_packetout(&filler, &packet); | ||||
if (!file->tpackets && (th_decode_headerin( | ||||
&file->tinfo, | ||||
&file->tcomment, | ||||
&tsetup, | ||||
&packet | ||||
) >= 0)) { | ||||
memcpy(&file->tstream, &filler, sizeof(filler)); | ||||
file->tpackets = 1; | ||||
} | ||||
else if (!file->vpackets && (vorbis_synthesis_headerin( | ||||
&file->vinfo, | ||||
&file->vcomment, | ||||
&packet | ||||
) >= 0)) { | ||||
memcpy(&file->vstream, &filler, sizeof(filler)); | ||||
file->vpackets = 1; | ||||
} | ||||
else | ||||
{ | ||||
/* Whatever it is, we don't care about it */ | ||||
ogg_stream_clear(&filler); | ||||
} | ||||
} | ||||
/* No audio OR video? */ | ||||
TF_OPEN_ASSERT(!file->tpackets && !file->vpackets) | ||||
/* Apparently there are 2 more theora and 2 more vorbis headers next. */ | ||||
#define TPACKETS (file->tpackets && (file->tpackets < 3)) | ||||
#define VPACKETS (file->vpackets && (file->vpackets < 3)) | ||||
while (TPACKETS || VPACKETS) | ||||
{ | ||||
while (TPACKETS) | ||||
{ | ||||
if (ogg_stream_packetout( | ||||
&file->tstream, | ||||
&packet | ||||
) != 1) { | ||||
/* Get more data? */ | ||||
break; | ||||
} | ||||
TF_OPEN_ASSERT(!th_decode_headerin( | ||||
&file->tinfo, | ||||
&file->tcomment, | ||||
&tsetup, | ||||
&packet | ||||
)) | ||||
file->tpackets += 1; | ||||
} | ||||
while (VPACKETS) | ||||
{ | ||||
if (ogg_stream_packetout( | ||||
&file->vstream, | ||||
&packet | ||||
) != 1) { | ||||
/* Get more data? */ | ||||
break; | ||||
} | ||||
TF_OPEN_ASSERT(vorbis_synthesis_headerin( | ||||
&file->vinfo, | ||||
&file->vcomment, | ||||
&packet | ||||
)) | ||||
file->vpackets += 1; | ||||
} | ||||
/* Get another page, try again? */ | ||||
if (ogg_sync_pageout(&file->sync, &file->page) > 0) | ||||
{ | ||||
INTERNAL_queueOggPage(file); | ||||
} | ||||
else | ||||
{ | ||||
TF_OPEN_ASSERT(INTERNAL_readOggData(file) < 0) | ||||
} | ||||
} | ||||
#undef TPACKETS | ||||
#undef VPACKETS | ||||
/* Set up Theora stream */ | ||||
if (file->tpackets) | ||||
{ | ||||
/* th_decode_alloc() docs say to check for | ||||
* insanely large frames yourself. | ||||
*/ | ||||
TF_OPEN_ASSERT( | ||||
(file->tinfo.frame_width > 99999) || | ||||
(file->tinfo.frame_height > 99999) | ||||
) | ||||
/* FIXME: We treat "unspecified" as NTSC :shrug: */ | ||||
if ( (file->tinfo.colorspace != TH_CS_UNSPECIFIED) && | ||||
(file->tinfo.colorspace != TH_CS_ITU_REC_470M) && | ||||
(file->tinfo.colorspace != TH_CS_ITU_REC_470BG) ) | ||||
{ | ||||
errcode = TF_EUNSUPPORTED; | ||||
goto fail; | ||||
} | ||||
if ( file->tinfo.pixel_fmt != TH_PF_420 && | ||||
file->tinfo.pixel_fmt != TH_PF_422 && | ||||
file->tinfo.pixel_fmt != TH_PF_444 ) | ||||
{ | ||||
errcode = TF_EUNSUPPORTED; | ||||
goto fail; | ||||
} | ||||
/* The decoder, at last! */ | ||||
file->tdec = th_decode_alloc(&file->tinfo, tsetup); | ||||
TF_OPEN_ASSERT(!file->tdec) | ||||
/* Disable all post-processing in the decoder. | ||||
* FIXME: Maybe an API to set this? | ||||
* FIXME: Could be TH_DECCTL_GET_PPLEVEL_MAX, for example! | ||||
* FIXME: Theoretically we could enable post-processing and then | ||||
* FIXME: drop the quality level if we're not keeping up. | ||||
*/ | ||||
th_decode_ctl( | ||||
file->tdec, | ||||
TH_DECCTL_SET_PPLEVEL, | ||||
&pp_level_max, | ||||
sizeof(pp_level_max) | ||||
); | ||||
} | ||||
/* Done with this now */ | ||||
if (tsetup != NULL) | ||||
{ | ||||
th_setup_free(tsetup); | ||||
tsetup = NULL; | ||||
} | ||||
/* Set up Vorbis stream */ | ||||
if (file->vpackets) | ||||
{ | ||||
file->vdsp_init = vorbis_synthesis_init( | ||||
&file->vdsp, | ||||
&file->vinfo | ||||
) == 0; | ||||
TF_OPEN_ASSERT(!file->vdsp_init) | ||||
file->vblock_init = vorbis_block_init( | ||||
&file->vdsp, | ||||
&file->vblock | ||||
) == 0; | ||||
TF_OPEN_ASSERT(!file->vblock_init) | ||||
} | ||||
#undef TF_OPEN_ASSERT | ||||
/* Finally. */ | ||||
return 0; | ||||
fail: | ||||
if (tsetup != NULL) | ||||
{ | ||||
th_setup_free(tsetup); | ||||
} | ||||
tf_close(file); | ||||
return errcode; | ||||
} | ||||
int tf_fopen(const char *fname, OggTheora_File *file) | ||||
{ | ||||
tf_callbacks io = | ||||
{ | ||||
(size_t (*) (void*, size_t, size_t, void*)) fread, | ||||
(int (*) (void*, ogg_int64_t, int)) fseek, | ||||
(int (*) (void*)) fclose, | ||||
}; | ||||
return tf_open_callbacks( | ||||
fopen(fname, "rb"), | ||||
file, | ||||
io | ||||
); | ||||
} | ||||
void tf_close(OggTheora_File *file) | ||||
{ | ||||
/* Theora Data */ | ||||
if (file->tdec != NULL) | ||||
{ | ||||
th_decode_free(file->tdec); | ||||
} | ||||
/* Vorbis Data */ | ||||
if (file->vblock_init) | ||||
{ | ||||
vorbis_block_clear(&file->vblock); | ||||
} | ||||
if (file->vdsp_init) | ||||
{ | ||||
vorbis_dsp_clear(&file->vdsp); | ||||
} | ||||
/* Stream Data */ | ||||
if (file->tpackets) | ||||
{ | ||||
ogg_stream_clear(&file->tstream); | ||||
} | ||||
if (file->vpackets) | ||||
{ | ||||
ogg_stream_clear(&file->vstream); | ||||
} | ||||
/* Metadata */ | ||||
th_info_clear(&file->tinfo); | ||||
th_comment_clear(&file->tcomment); | ||||
vorbis_comment_clear(&file->vcomment); | ||||
vorbis_info_clear(&file->vinfo); | ||||
/* Current State */ | ||||
ogg_sync_clear(&file->sync); | ||||
/* I/O Data */ | ||||
if (file->io.close_func != NULL) | ||||
{ | ||||
file->io.close_func(file->datasource); | ||||
} | ||||
} | ||||
int tf_hasvideo(OggTheora_File *file) | ||||
{ | ||||
return file->tpackets != 0; | ||||
} | ||||
int tf_hasaudio(OggTheora_File *file) | ||||
{ | ||||
return file->vpackets != 0; | ||||
} | ||||
void tf_videoinfo( | ||||
OggTheora_File *file, | ||||
int *width, | ||||
int *height, | ||||
double *fps, | ||||
th_pixel_fmt *fmt | ||||
) { | ||||
if (width != NULL) | ||||
{ | ||||
*width = file->tinfo.pic_width; | ||||
} | ||||
if (height != NULL) | ||||
{ | ||||
*height = file->tinfo.pic_height; | ||||
} | ||||
if (fps != NULL) | ||||
{ | ||||
if (file->tinfo.fps_denominator != 0) | ||||
{ | ||||
*fps = ( | ||||
((double) file->tinfo.fps_numerator) / | ||||
((double) file->tinfo.fps_denominator) | ||||
); | ||||
} | ||||
else | ||||
{ | ||||
*fps = 0.0; | ||||
} | ||||
} | ||||
if (fmt != NULL) | ||||
{ | ||||
*fmt = file->tinfo.pixel_fmt; | ||||
} | ||||
} | ||||
void tf_audioinfo(OggTheora_File *file, int *channels, int *samplerate) | ||||
{ | ||||
if (channels != NULL) | ||||
{ | ||||
*channels = file->vinfo.channels; | ||||
} | ||||
if (samplerate != NULL) | ||||
{ | ||||
*samplerate = file->vinfo.rate; | ||||
} | ||||
} | ||||
int tf_eos(OggTheora_File *file) | ||||
{ | ||||
return file->eos; | ||||
} | ||||
void tf_reset(OggTheora_File *file) | ||||
{ | ||||
if (file->tpackets) | ||||
{ | ||||
ogg_stream_reset(&file->tstream); | ||||
} | ||||
if (file->vpackets) | ||||
{ | ||||
ogg_stream_reset(&file->vstream); | ||||
} | ||||
ogg_sync_reset(&file->sync); | ||||
file->io.seek_func(file->datasource, 0, SEEK_SET); | ||||
file->eos = 0; | ||||
} | ||||
int tf_readvideo(OggTheora_File *file, char *buffer, int numframes) | ||||
{ | ||||
int i; | ||||
char *dst = buffer; | ||||
ogg_int64_t granulepos = 0; | ||||
ogg_packet packet; | ||||
th_ycbcr_buffer ycbcr; | ||||
int rc; | ||||
int w, h, off; | ||||
unsigned char *plane; | ||||
int stride; | ||||
int retval = 0; | ||||
for (i = 0; i < numframes; i += 1) | ||||
{ | ||||
/* Keep trying to get a usable packet */ | ||||
if (!INTERNAL_getNextPacket(file, &file->tstream, &packet)) | ||||
{ | ||||
/* ... unless there's nothing left for us to read. */ | ||||
if (retval) | ||||
{ | ||||
break; | ||||
} | ||||
return 0; | ||||
} | ||||
rc = th_decode_packetin( | ||||
file->tdec, | ||||
&packet, | ||||
&granulepos | ||||
); | ||||
if (rc == 0) /* New frame! */ | ||||
{ | ||||
retval = 1; | ||||
} | ||||
else if (rc != TH_DUPFRAME) | ||||
{ | ||||
return 0; /* Why did we get here...? */ | ||||
} | ||||
} | ||||
if (retval) /* New frame! */ | ||||
{ | ||||
if (th_decode_ycbcr_out(file->tdec, ycbcr) != 0) | ||||
{ | ||||
return 0; /* Uhh?! */ | ||||
} | ||||
#define TF_COPY_CHANNEL(chan) \ | ||||
plane = ycbcr[chan].data + off; \ | ||||
stride = ycbcr[chan].stride; \ | ||||
for (i = 0; i < h; i += 1, dst += w) \ | ||||
{ \ | ||||
memcpy( \ | ||||
dst, \ | ||||
plane + (stride * i), \ | ||||
w \ | ||||
); \ | ||||
} | ||||
/* Y */ | ||||
w = file->tinfo.pic_width; | ||||
h = file->tinfo.pic_height; | ||||
off = ( | ||||
(file->tinfo.pic_x & ~1) + | ||||
ycbcr[0].stride * | ||||
(file->tinfo.pic_y & ~1) | ||||
); | ||||
TF_COPY_CHANNEL(0) | ||||
/* U/V */ | ||||
if (file->tinfo.pixel_fmt == TH_PF_420) | ||||
{ | ||||
/* Subsampled in both dimensions */ | ||||
w /= 2; | ||||
h /= 2; | ||||
off = ( | ||||
(file->tinfo.pic_x / 2) + | ||||
(ycbcr[1].stride) * | ||||
(file->tinfo.pic_y / 2) | ||||
); | ||||
} | ||||
else if (file->tinfo.pixel_fmt == TH_PF_422) | ||||
{ | ||||
/* Subsampled only horizontally */ | ||||
w /= 2; | ||||
off = ( | ||||
(file->tinfo.pic_x / 2) + | ||||
(ycbcr[1].stride) * | ||||
(file->tinfo.pic_y & ~1) | ||||
); | ||||
} | ||||
TF_COPY_CHANNEL(1) | ||||
TF_COPY_CHANNEL(2) | ||||
#undef TF_COPY_CHANNEL | ||||
} | ||||
return retval; | ||||
} | ||||
int tf_readaudio(OggTheora_File *file, float *buffer, int samples) | ||||
{ | ||||
int offset = 0; | ||||
int chan, frame; | ||||
ogg_packet packet; | ||||
float **pcm = NULL; | ||||
while (offset < samples) | ||||
{ | ||||
const int frames = vorbis_synthesis_pcmout(&file->vdsp, &pcm); | ||||
if (frames > 0) | ||||
{ | ||||
/* I bet this beats the crap out of the CPU cache... */ | ||||
for (frame = 0; frame < frames; frame += 1) | ||||
for (chan = 0; chan < file->vinfo.channels; chan += 1) | ||||
{ | ||||
buffer[offset++] = pcm[chan][frame]; | ||||
if (offset >= samples) | ||||
{ | ||||
vorbis_synthesis_read( | ||||
&file->vdsp, | ||||
frame | ||||
); | ||||
return offset; | ||||
} | ||||
} | ||||
vorbis_synthesis_read(&file->vdsp, frames); | ||||
} | ||||
else /* No audio available left in current packet? */ | ||||
{ | ||||
/* Keep trying to get a usable packet */ | ||||
if (!INTERNAL_getNextPacket(file, &file->vstream, &packet)) | ||||
{ | ||||
/* ... unless there's nothing left for us to read. */ | ||||
return offset; | ||||
} | ||||
if (vorbis_synthesis( | ||||
&file->vblock, | ||||
&packet | ||||
) == 0) { | ||||
vorbis_synthesis_blockin( | ||||
&file->vdsp, | ||||
&file->vblock | ||||
); | ||||
} | ||||
} | ||||
} | ||||
return offset; | ||||
} | ||||