mirror of
https://github.com/Relintai/pandemonium_engine.git
synced 2024-12-27 06:07:14 +01:00
3267 lines
122 KiB
C
3267 lines
122 KiB
C
/********************************************************************
|
|
* *
|
|
* THIS FILE IS PART OF THE libopusfile 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 libopusfile SOURCE CODE IS (C) COPYRIGHT 1994-2012 *
|
|
* by the Xiph.Org Foundation and contributors http://www.xiph.org/ *
|
|
* *
|
|
********************************************************************
|
|
|
|
function: stdio-based convenience library for opening/seeking/decoding
|
|
last mod: $Id: vorbisfile.c 17573 2010-10-27 14:53:59Z xiphmont $
|
|
|
|
********************************************************************/
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include "internal.h"
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <errno.h>
|
|
#include <limits.h>
|
|
#include <string.h>
|
|
#include <math.h>
|
|
|
|
#include "opusfile.h"
|
|
|
|
/*This implementation is largely based off of libvorbisfile.
|
|
All of the Ogg bits work roughly the same, though I have made some
|
|
"improvements" that have not been folded back there, yet.*/
|
|
|
|
/*A 'chained bitstream' is an Ogg Opus bitstream that contains more than one
|
|
logical bitstream arranged end to end (the only form of Ogg multiplexing
|
|
supported by this library.
|
|
Grouping (parallel multiplexing) is not supported, except to the extent that
|
|
if there are multiple logical Ogg streams in a single link of the chain, we
|
|
will ignore all but the first Opus stream we find.*/
|
|
|
|
/*An Ogg Opus file can be played beginning to end (streamed) without worrying
|
|
ahead of time about chaining (see opusdec from the opus-tools package).
|
|
If we have the whole file, however, and want random access
|
|
(seeking/scrubbing) or desire to know the total length/time of a file, we
|
|
need to account for the possibility of chaining.*/
|
|
|
|
/*We can handle things a number of ways.
|
|
We can determine the entire bitstream structure right off the bat, or find
|
|
pieces on demand.
|
|
This library determines and caches structure for the entire bitstream, but
|
|
builds a virtual decoder on the fly when moving between links in the chain.*/
|
|
|
|
/*There are also different ways to implement seeking.
|
|
Enough information exists in an Ogg bitstream to seek to sample-granularity
|
|
positions in the output.
|
|
Or, one can seek by picking some portion of the stream roughly in the desired
|
|
area if we only want coarse navigation through the stream.
|
|
We implement and expose both strategies.*/
|
|
|
|
/*The maximum number of bytes in a page (including the page headers).*/
|
|
#define OP_PAGE_SIZE_MAX (65307)
|
|
/*The default amount to seek backwards per step when trying to find the
|
|
previous page.
|
|
This must be at least as large as the maximum size of a page.*/
|
|
#define OP_CHUNK_SIZE (65536)
|
|
/*The maximum amount to seek backwards per step when trying to find the
|
|
previous page.*/
|
|
#define OP_CHUNK_SIZE_MAX (1024*(opus_int32)1024)
|
|
/*A smaller read size is needed for low-rate streaming.*/
|
|
#define OP_READ_SIZE (2048)
|
|
|
|
int op_test(OpusHead *_head,
|
|
const unsigned char *_initial_data,size_t _initial_bytes){
|
|
ogg_sync_state oy;
|
|
char *data;
|
|
int err;
|
|
/*The first page of a normal Opus file will be at most 57 bytes (27 Ogg
|
|
page header bytes + 1 lacing value + 21 Opus header bytes + 8 channel
|
|
mapping bytes).
|
|
It will be at least 47 bytes (27 Ogg page header bytes + 1 lacing value +
|
|
19 Opus header bytes using channel mapping family 0).
|
|
If we don't have at least that much data, give up now.*/
|
|
if(_initial_bytes<47)return OP_FALSE;
|
|
/*Only proceed if we start with the magic OggS string.
|
|
This is to prevent us spending a lot of time allocating memory and looking
|
|
for Ogg pages in non-Ogg files.*/
|
|
if(memcmp(_initial_data,"OggS",4)!=0)return OP_ENOTFORMAT;
|
|
ogg_sync_init(&oy);
|
|
data=ogg_sync_buffer(&oy,_initial_bytes);
|
|
if(data!=NULL){
|
|
ogg_stream_state os;
|
|
ogg_page og;
|
|
int ret;
|
|
memcpy(data,_initial_data,_initial_bytes);
|
|
ogg_sync_wrote(&oy,_initial_bytes);
|
|
ogg_stream_init(&os,-1);
|
|
err=OP_FALSE;
|
|
do{
|
|
ogg_packet op;
|
|
ret=ogg_sync_pageout(&oy,&og);
|
|
/*Ignore holes.*/
|
|
if(ret<0)continue;
|
|
/*Stop if we run out of data.*/
|
|
if(!ret)break;
|
|
ogg_stream_reset_serialno(&os,ogg_page_serialno(&og));
|
|
ogg_stream_pagein(&os,&og);
|
|
/*Only process the first packet on this page (if it's a BOS packet,
|
|
it's required to be the only one).*/
|
|
if(ogg_stream_packetout(&os,&op)==1){
|
|
if(op.b_o_s){
|
|
ret=opus_head_parse(_head,op.packet,op.bytes);
|
|
/*If this didn't look like Opus, keep going.*/
|
|
if(ret==OP_ENOTFORMAT)continue;
|
|
/*Otherwise we're done, one way or another.*/
|
|
err=ret;
|
|
}
|
|
/*We finished parsing the headers.
|
|
There is no Opus to be found.*/
|
|
else err=OP_ENOTFORMAT;
|
|
}
|
|
}
|
|
while(err==OP_FALSE);
|
|
ogg_stream_clear(&os);
|
|
}
|
|
else err=OP_EFAULT;
|
|
ogg_sync_clear(&oy);
|
|
return err;
|
|
}
|
|
|
|
/*Many, many internal helpers.
|
|
The intention is not to be confusing.
|
|
Rampant duplication and monolithic function implementation (though we do have
|
|
some large, omnibus functions still) would be harder to understand anyway.
|
|
The high level functions are last.
|
|
Begin grokking near the end of the file if you prefer to read things
|
|
top-down.*/
|
|
|
|
/*The read/seek functions track absolute position within the stream.*/
|
|
|
|
/*Read a little more data from the file/pipe into the ogg_sync framer.
|
|
_nbytes: The maximum number of bytes to read.
|
|
Return: A positive number of bytes read on success, 0 on end-of-file, or a
|
|
negative value on failure.*/
|
|
static int op_get_data(OggOpusFile *_of,int _nbytes){
|
|
unsigned char *buffer;
|
|
int nbytes;
|
|
OP_ASSERT(_nbytes>0);
|
|
buffer=(unsigned char *)ogg_sync_buffer(&_of->oy,_nbytes);
|
|
nbytes=(int)(*_of->callbacks.read)(_of->source,buffer,_nbytes);
|
|
OP_ASSERT(nbytes<=_nbytes);
|
|
if(OP_LIKELY(nbytes>0))ogg_sync_wrote(&_of->oy,nbytes);
|
|
return nbytes;
|
|
}
|
|
|
|
/*Save a tiny smidge of verbosity to make the code more readable.*/
|
|
static int op_seek_helper(OggOpusFile *_of,opus_int64 _offset){
|
|
if(_offset==_of->offset)return 0;
|
|
if(_of->callbacks.seek==NULL
|
|
||(*_of->callbacks.seek)(_of->source,_offset,SEEK_SET)){
|
|
return OP_EREAD;
|
|
}
|
|
_of->offset=_offset;
|
|
ogg_sync_reset(&_of->oy);
|
|
return 0;
|
|
}
|
|
|
|
/*Get the current position indicator of the underlying source.
|
|
This should be the same as the value reported by tell().*/
|
|
static opus_int64 op_position(const OggOpusFile *_of){
|
|
/*The current position indicator is _not_ simply offset.
|
|
We may also have unprocessed, buffered data in the sync state.*/
|
|
return _of->offset+_of->oy.fill-_of->oy.returned;
|
|
}
|
|
|
|
/*From the head of the stream, get the next page.
|
|
_boundary specifies if the function is allowed to fetch more data from the
|
|
stream (and how much) or only use internally buffered data.
|
|
_boundary: -1: Unbounded search.
|
|
0: Read no additional data.
|
|
Use only cached data.
|
|
n: Search for the start of a new page up to file position n.
|
|
Return: n>=0: Found a page at absolute offset n.
|
|
OP_FALSE: Hit the _boundary limit.
|
|
OP_EREAD: An underlying read operation failed.
|
|
OP_BADLINK: We hit end-of-file before reaching _boundary.*/
|
|
static opus_int64 op_get_next_page(OggOpusFile *_of,ogg_page *_og,
|
|
opus_int64 _boundary){
|
|
while(_boundary<=0||_of->offset<_boundary){
|
|
int more;
|
|
more=ogg_sync_pageseek(&_of->oy,_og);
|
|
/*Skipped (-more) bytes.*/
|
|
if(OP_UNLIKELY(more<0))_of->offset-=more;
|
|
else if(more==0){
|
|
int read_nbytes;
|
|
int ret;
|
|
/*Send more paramedics.*/
|
|
if(!_boundary)return OP_FALSE;
|
|
if(_boundary<0)read_nbytes=OP_READ_SIZE;
|
|
else{
|
|
opus_int64 position;
|
|
position=op_position(_of);
|
|
if(position>=_boundary)return OP_FALSE;
|
|
read_nbytes=(int)OP_MIN(_boundary-position,OP_READ_SIZE);
|
|
}
|
|
ret=op_get_data(_of,read_nbytes);
|
|
if(OP_UNLIKELY(ret<0))return OP_EREAD;
|
|
if(OP_UNLIKELY(ret==0)){
|
|
/*Only fail cleanly on EOF if we didn't have a known boundary.
|
|
Otherwise, we should have been able to reach that boundary, and this
|
|
is a fatal error.*/
|
|
return OP_UNLIKELY(_boundary<0)?OP_FALSE:OP_EBADLINK;
|
|
}
|
|
}
|
|
else{
|
|
/*Got a page.
|
|
Return the page start offset and advance the internal offset past the
|
|
page end.*/
|
|
opus_int64 page_offset;
|
|
page_offset=_of->offset;
|
|
_of->offset+=more;
|
|
OP_ASSERT(page_offset>=0);
|
|
return page_offset;
|
|
}
|
|
}
|
|
return OP_FALSE;
|
|
}
|
|
|
|
static int op_add_serialno(const ogg_page *_og,
|
|
ogg_uint32_t **_serialnos,int *_nserialnos,int *_cserialnos){
|
|
ogg_uint32_t *serialnos;
|
|
int nserialnos;
|
|
int cserialnos;
|
|
ogg_uint32_t s;
|
|
s=ogg_page_serialno(_og);
|
|
serialnos=*_serialnos;
|
|
nserialnos=*_nserialnos;
|
|
cserialnos=*_cserialnos;
|
|
if(OP_UNLIKELY(nserialnos>=cserialnos)){
|
|
if(OP_UNLIKELY(cserialnos>INT_MAX/(int)sizeof(*serialnos)-1>>1)){
|
|
return OP_EFAULT;
|
|
}
|
|
cserialnos=2*cserialnos+1;
|
|
OP_ASSERT(nserialnos<cserialnos);
|
|
serialnos=(ogg_uint32_t *)_ogg_realloc(serialnos,
|
|
sizeof(*serialnos)*cserialnos);
|
|
if(OP_UNLIKELY(serialnos==NULL))return OP_EFAULT;
|
|
}
|
|
serialnos[nserialnos++]=s;
|
|
*_serialnos=serialnos;
|
|
*_nserialnos=nserialnos;
|
|
*_cserialnos=cserialnos;
|
|
return 0;
|
|
}
|
|
|
|
/*Returns nonzero if found.*/
|
|
static int op_lookup_serialno(ogg_uint32_t _s,
|
|
const ogg_uint32_t *_serialnos,int _nserialnos){
|
|
int i;
|
|
for(i=0;i<_nserialnos&&_serialnos[i]!=_s;i++);
|
|
return i<_nserialnos;
|
|
}
|
|
|
|
static int op_lookup_page_serialno(const ogg_page *_og,
|
|
const ogg_uint32_t *_serialnos,int _nserialnos){
|
|
return op_lookup_serialno(ogg_page_serialno(_og),_serialnos,_nserialnos);
|
|
}
|
|
|
|
typedef struct OpusSeekRecord OpusSeekRecord;
|
|
|
|
/*We use this to remember the pages we found while enumerating the links of a
|
|
chained stream.
|
|
We keep track of the starting and ending offsets, as well as the point we
|
|
started searching from, so we know where to bisect.
|
|
We also keep the serial number, so we can tell if the page belonged to the
|
|
current link or not, as well as the granule position, to aid in estimating
|
|
the start of the link.*/
|
|
struct OpusSeekRecord{
|
|
/*The earliest byte we know of such that reading forward from it causes
|
|
capture to be regained at this page.*/
|
|
opus_int64 search_start;
|
|
/*The offset of this page.*/
|
|
opus_int64 offset;
|
|
/*The size of this page.*/
|
|
opus_int32 size;
|
|
/*The serial number of this page.*/
|
|
ogg_uint32_t serialno;
|
|
/*The granule position of this page.*/
|
|
ogg_int64_t gp;
|
|
};
|
|
|
|
/*Find the last page beginning before _offset with a valid granule position.
|
|
There is no '_boundary' parameter as it will always have to read more data.
|
|
This is much dirtier than the above, as Ogg doesn't have any backward search
|
|
linkage.
|
|
This search prefers pages of the specified serial number.
|
|
If a page of the specified serial number is spotted during the
|
|
seek-back-and-read-forward, it will return the info of last page of the
|
|
matching serial number, instead of the very last page, unless the very last
|
|
page belongs to a different link than preferred serial number.
|
|
If no page of the specified serial number is seen, it will return the info of
|
|
the last page.
|
|
[out] _sr: Returns information about the page that was found on success.
|
|
_offset: The _offset before which to find a page.
|
|
Any page returned will consist of data entirely before _offset.
|
|
_serialno: The preferred serial number.
|
|
If a page with this serial number is found, it will be returned
|
|
even if another page in the same link is found closer to
|
|
_offset.
|
|
This is purely opportunistic: there is no guarantee such a page
|
|
will be found if it exists.
|
|
_serialnos: The list of serial numbers in the link that contains the
|
|
preferred serial number.
|
|
_nserialnos: The number of serial numbers in the current link.
|
|
Return: 0 on success, or a negative value on failure.
|
|
OP_EREAD: Failed to read more data (error or EOF).
|
|
OP_EBADLINK: We couldn't find a page even after seeking back to the
|
|
start of the stream.*/
|
|
static int op_get_prev_page_serial(OggOpusFile *_of,OpusSeekRecord *_sr,
|
|
opus_int64 _offset,ogg_uint32_t _serialno,
|
|
const ogg_uint32_t *_serialnos,int _nserialnos){
|
|
OpusSeekRecord preferred_sr;
|
|
ogg_page og;
|
|
opus_int64 begin;
|
|
opus_int64 end;
|
|
opus_int64 original_end;
|
|
opus_int32 chunk_size;
|
|
int preferred_found;
|
|
original_end=end=begin=_offset;
|
|
preferred_found=0;
|
|
_offset=-1;
|
|
chunk_size=OP_CHUNK_SIZE;
|
|
do{
|
|
opus_int64 search_start;
|
|
int ret;
|
|
OP_ASSERT(chunk_size>=OP_PAGE_SIZE_MAX);
|
|
begin=OP_MAX(begin-chunk_size,0);
|
|
ret=op_seek_helper(_of,begin);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
search_start=begin;
|
|
while(_of->offset<end){
|
|
opus_int64 llret;
|
|
ogg_uint32_t serialno;
|
|
llret=op_get_next_page(_of,&og,end);
|
|
if(OP_UNLIKELY(llret<OP_FALSE))return (int)llret;
|
|
else if(llret==OP_FALSE)break;
|
|
serialno=ogg_page_serialno(&og);
|
|
/*Save the information for this page.
|
|
We're not interested in the page itself... just the serial number, byte
|
|
offset, page size, and granule position.*/
|
|
_sr->search_start=search_start;
|
|
_sr->offset=_offset=llret;
|
|
_sr->serialno=serialno;
|
|
OP_ASSERT(_of->offset-_offset>=0);
|
|
OP_ASSERT(_of->offset-_offset<=OP_PAGE_SIZE_MAX);
|
|
_sr->size=(opus_int32)(_of->offset-_offset);
|
|
_sr->gp=ogg_page_granulepos(&og);
|
|
/*If this page is from the stream we're looking for, remember it.*/
|
|
if(serialno==_serialno){
|
|
preferred_found=1;
|
|
*&preferred_sr=*_sr;
|
|
}
|
|
if(!op_lookup_serialno(serialno,_serialnos,_nserialnos)){
|
|
/*We fell off the end of the link, which means we seeked back too far
|
|
and shouldn't have been looking in that link to begin with.
|
|
If we found the preferred serial number, forget that we saw it.*/
|
|
preferred_found=0;
|
|
}
|
|
search_start=llret+1;
|
|
}
|
|
/*We started from the beginning of the stream and found nothing.
|
|
This should be impossible unless the contents of the source changed out
|
|
from under us after we read from it.*/
|
|
if(OP_UNLIKELY(!begin)&&OP_UNLIKELY(_offset<0))return OP_EBADLINK;
|
|
/*Bump up the chunk size.
|
|
This is mildly helpful when seeks are very expensive (http).*/
|
|
chunk_size=OP_MIN(2*chunk_size,OP_CHUNK_SIZE_MAX);
|
|
/*Avoid quadratic complexity if we hit an invalid patch of the file.*/
|
|
end=OP_MIN(begin+OP_PAGE_SIZE_MAX-1,original_end);
|
|
}
|
|
while(_offset<0);
|
|
if(preferred_found)*_sr=*&preferred_sr;
|
|
return 0;
|
|
}
|
|
|
|
/*Find the last page beginning before _offset with the given serial number and
|
|
a valid granule position.
|
|
Unlike the above search, this continues until it finds such a page, but does
|
|
not stray outside the current link.
|
|
We could implement it (inefficiently) by calling op_get_prev_page_serial()
|
|
repeatedly until it returned a page that had both our preferred serial
|
|
number and a valid granule position, but doing it with a separate function
|
|
allows us to avoid repeatedly re-scanning valid pages from other streams as
|
|
we seek-back-and-read-forward.
|
|
[out] _gp: Returns the granule position of the page that was found on
|
|
success.
|
|
_offset: The _offset before which to find a page.
|
|
Any page returned will consist of data entirely before _offset.
|
|
_serialno: The target serial number.
|
|
_serialnos: The list of serial numbers in the link that contains the
|
|
preferred serial number.
|
|
_nserialnos: The number of serial numbers in the current link.
|
|
Return: The offset of the page on success, or a negative value on failure.
|
|
OP_EREAD: Failed to read more data (error or EOF).
|
|
OP_EBADLINK: We couldn't find a page even after seeking back past the
|
|
beginning of the link.*/
|
|
static opus_int64 op_get_last_page(OggOpusFile *_of,ogg_int64_t *_gp,
|
|
opus_int64 _offset,ogg_uint32_t _serialno,
|
|
const ogg_uint32_t *_serialnos,int _nserialnos){
|
|
ogg_page og;
|
|
ogg_int64_t gp;
|
|
opus_int64 begin;
|
|
opus_int64 end;
|
|
opus_int64 original_end;
|
|
opus_int32 chunk_size;
|
|
/*The target serial number must belong to the current link.*/
|
|
OP_ASSERT(op_lookup_serialno(_serialno,_serialnos,_nserialnos));
|
|
original_end=end=begin=_offset;
|
|
_offset=-1;
|
|
/*We shouldn't have to initialize gp, but gcc is too dumb to figure out that
|
|
ret>=0 implies we entered the if(page_gp!=-1) block at least once.*/
|
|
gp=-1;
|
|
chunk_size=OP_CHUNK_SIZE;
|
|
do{
|
|
int left_link;
|
|
int ret;
|
|
OP_ASSERT(chunk_size>=OP_PAGE_SIZE_MAX);
|
|
begin=OP_MAX(begin-chunk_size,0);
|
|
ret=op_seek_helper(_of,begin);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
left_link=0;
|
|
while(_of->offset<end){
|
|
opus_int64 llret;
|
|
ogg_uint32_t serialno;
|
|
llret=op_get_next_page(_of,&og,end);
|
|
if(OP_UNLIKELY(llret<OP_FALSE))return llret;
|
|
else if(llret==OP_FALSE)break;
|
|
serialno=ogg_page_serialno(&og);
|
|
if(serialno==_serialno){
|
|
ogg_int64_t page_gp;
|
|
/*The page is from the right stream...*/
|
|
page_gp=ogg_page_granulepos(&og);
|
|
if(page_gp!=-1){
|
|
/*And has a valid granule position.
|
|
Let's remember it.*/
|
|
_offset=llret;
|
|
gp=page_gp;
|
|
}
|
|
}
|
|
else if(OP_UNLIKELY(!op_lookup_serialno(serialno,
|
|
_serialnos,_nserialnos))){
|
|
/*We fell off the start of the link, which means we don't need to keep
|
|
seeking any farther back.*/
|
|
left_link=1;
|
|
}
|
|
}
|
|
/*We started from at or before the beginning of the link and found nothing.
|
|
This should be impossible unless the contents of the source changed out
|
|
from under us after we read from it.*/
|
|
if((OP_UNLIKELY(left_link)||OP_UNLIKELY(!begin))&&OP_UNLIKELY(_offset<0)){
|
|
return OP_EBADLINK;
|
|
}
|
|
/*Bump up the chunk size.
|
|
This is mildly helpful when seeks are very expensive (http).*/
|
|
chunk_size=OP_MIN(2*chunk_size,OP_CHUNK_SIZE_MAX);
|
|
/*Avoid quadratic complexity if we hit an invalid patch of the file.*/
|
|
end=OP_MIN(begin+OP_PAGE_SIZE_MAX-1,original_end);
|
|
}
|
|
while(_offset<0);
|
|
*_gp=gp;
|
|
return _offset;
|
|
}
|
|
|
|
/*Uses the local ogg_stream storage in _of.
|
|
This is important for non-streaming input sources.*/
|
|
static int op_fetch_headers_impl(OggOpusFile *_of,OpusHead *_head,
|
|
OpusTags *_tags,ogg_uint32_t **_serialnos,int *_nserialnos,
|
|
int *_cserialnos,ogg_page *_og){
|
|
ogg_packet op;
|
|
int ret;
|
|
if(_serialnos!=NULL)*_nserialnos=0;
|
|
/*Extract the serialnos of all BOS pages plus the first set of Opus headers
|
|
we see in the link.*/
|
|
while(ogg_page_bos(_og)){
|
|
if(_serialnos!=NULL){
|
|
if(OP_UNLIKELY(op_lookup_page_serialno(_og,*_serialnos,*_nserialnos))){
|
|
/*A dupe serialnumber in an initial header packet set==invalid stream.*/
|
|
return OP_EBADHEADER;
|
|
}
|
|
ret=op_add_serialno(_og,_serialnos,_nserialnos,_cserialnos);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
}
|
|
if(_of->ready_state<OP_STREAMSET){
|
|
/*We don't have an Opus stream in this link yet, so begin prospective
|
|
stream setup.
|
|
We need a stream to get packets.*/
|
|
ogg_stream_reset_serialno(&_of->os,ogg_page_serialno(_og));
|
|
ogg_stream_pagein(&_of->os,_og);
|
|
if(OP_LIKELY(ogg_stream_packetout(&_of->os,&op)>0)){
|
|
ret=opus_head_parse(_head,op.packet,op.bytes);
|
|
/*Found a valid Opus header.
|
|
Continue setup.*/
|
|
if(OP_LIKELY(ret>=0))_of->ready_state=OP_STREAMSET;
|
|
/*If it's just a stream type we don't recognize, ignore it.
|
|
Everything else is fatal.*/
|
|
else if(ret!=OP_ENOTFORMAT)return ret;
|
|
}
|
|
/*TODO: Should a BOS page with no packets be an error?*/
|
|
}
|
|
/*Get the next page.
|
|
No need to clamp the boundary offset against _of->end, as all errors
|
|
become OP_ENOTFORMAT or OP_EBADHEADER.*/
|
|
if(OP_UNLIKELY(op_get_next_page(_of,_og,
|
|
OP_ADV_OFFSET(_of->offset,OP_CHUNK_SIZE))<0)){
|
|
return _of->ready_state<OP_STREAMSET?OP_ENOTFORMAT:OP_EBADHEADER;
|
|
}
|
|
}
|
|
if(OP_UNLIKELY(_of->ready_state!=OP_STREAMSET))return OP_ENOTFORMAT;
|
|
/*If the first non-header page belonged to our Opus stream, submit it.*/
|
|
if(_of->os.serialno==ogg_page_serialno(_og))ogg_stream_pagein(&_of->os,_og);
|
|
/*Loop getting packets.*/
|
|
for(;;){
|
|
switch(ogg_stream_packetout(&_of->os,&op)){
|
|
case 0:{
|
|
/*Loop getting pages.*/
|
|
for(;;){
|
|
/*No need to clamp the boundary offset against _of->end, as all
|
|
errors become OP_EBADHEADER.*/
|
|
if(OP_UNLIKELY(op_get_next_page(_of,_og,
|
|
OP_ADV_OFFSET(_of->offset,OP_CHUNK_SIZE))<0)){
|
|
return OP_EBADHEADER;
|
|
}
|
|
/*If this page belongs to the correct stream, go parse it.*/
|
|
if(_of->os.serialno==ogg_page_serialno(_og)){
|
|
ogg_stream_pagein(&_of->os,_og);
|
|
break;
|
|
}
|
|
/*If the link ends before we see the Opus comment header, abort.*/
|
|
if(OP_UNLIKELY(ogg_page_bos(_og)))return OP_EBADHEADER;
|
|
/*Otherwise, keep looking.*/
|
|
}
|
|
}break;
|
|
/*We shouldn't get a hole in the headers!*/
|
|
case -1:return OP_EBADHEADER;
|
|
default:{
|
|
/*Got a packet.
|
|
It should be the comment header.*/
|
|
ret=opus_tags_parse(_tags,op.packet,op.bytes);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
/*Make sure the page terminated at the end of the comment header.
|
|
If there is another packet on the page, or part of a packet, then
|
|
reject the stream.
|
|
Otherwise seekable sources won't be able to seek back to the start
|
|
properly.*/
|
|
ret=ogg_stream_packetout(&_of->os,&op);
|
|
if(OP_UNLIKELY(ret!=0)
|
|
||OP_UNLIKELY(_og->header[_og->header_len-1]==255)){
|
|
/*If we fail, the caller assumes our tags are uninitialized.*/
|
|
opus_tags_clear(_tags);
|
|
return OP_EBADHEADER;
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static int op_fetch_headers(OggOpusFile *_of,OpusHead *_head,
|
|
OpusTags *_tags,ogg_uint32_t **_serialnos,int *_nserialnos,
|
|
int *_cserialnos,ogg_page *_og){
|
|
ogg_page og;
|
|
int ret;
|
|
if(!_og){
|
|
/*No need to clamp the boundary offset against _of->end, as all errors
|
|
become OP_ENOTFORMAT.*/
|
|
if(OP_UNLIKELY(op_get_next_page(_of,&og,
|
|
OP_ADV_OFFSET(_of->offset,OP_CHUNK_SIZE))<0)){
|
|
return OP_ENOTFORMAT;
|
|
}
|
|
_og=&og;
|
|
}
|
|
_of->ready_state=OP_OPENED;
|
|
ret=op_fetch_headers_impl(_of,_head,_tags,_serialnos,_nserialnos,
|
|
_cserialnos,_og);
|
|
/*Revert back from OP_STREAMSET to OP_OPENED on failure, to prevent
|
|
double-free of the tags in an unseekable stream.*/
|
|
if(OP_UNLIKELY(ret<0))_of->ready_state=OP_OPENED;
|
|
return ret;
|
|
}
|
|
|
|
/*Granule position manipulation routines.
|
|
A granule position is defined to be an unsigned 64-bit integer, with the
|
|
special value -1 in two's complement indicating an unset or invalid granule
|
|
position.
|
|
We are not guaranteed to have an unsigned 64-bit type, so we construct the
|
|
following routines that
|
|
a) Properly order negative numbers as larger than positive numbers, and
|
|
b) Check for underflow or overflow past the special -1 value.
|
|
This lets us operate on the full, valid range of granule positions in a
|
|
consistent and safe manner.
|
|
This full range is organized into distinct regions:
|
|
[ -1 (invalid) ][ 0 ... OP_INT64_MAX ][ OP_INT64_MIN ... -2 ][-1 (invalid) ]
|
|
|
|
No one should actually use granule positions so large that they're negative,
|
|
even if they are technically valid, as very little software handles them
|
|
correctly (including most of Xiph.Org's).
|
|
This library also refuses to support durations so large they won't fit in a
|
|
signed 64-bit integer (to avoid exposing this mess to the application, and
|
|
to simplify a good deal of internal arithmetic), so the only way to use them
|
|
successfully is if pcm_start is very large.
|
|
This means there isn't anything you can do with negative granule positions
|
|
that you couldn't have done with purely non-negative ones.
|
|
The main purpose of these routines is to allow us to think very explicitly
|
|
about the possible failure cases of all granule position manipulations.*/
|
|
|
|
/*Safely adds a small signed integer to a valid (not -1) granule position.
|
|
The result can use the full 64-bit range of values (both positive and
|
|
negative), but will fail on overflow (wrapping past -1; wrapping past
|
|
OP_INT64_MAX is explicitly okay).
|
|
[out] _dst_gp: The resulting granule position.
|
|
Only modified on success.
|
|
_src_gp: The granule position to add to.
|
|
This must not be -1.
|
|
_delta: The amount to add.
|
|
This is allowed to be up to 32 bits to support the maximum
|
|
duration of a single Ogg page (255 packets * 120 ms per
|
|
packet == 1,468,800 samples at 48 kHz).
|
|
Return: 0 on success, or OP_EINVAL if the result would wrap around past -1.*/
|
|
static int op_granpos_add(ogg_int64_t *_dst_gp,ogg_int64_t _src_gp,
|
|
opus_int32 _delta){
|
|
/*The code below handles this case correctly, but there's no reason we
|
|
should ever be called with these values, so make sure we aren't.*/
|
|
OP_ASSERT(_src_gp!=-1);
|
|
if(_delta>0){
|
|
/*Adding this amount to the granule position would overflow its 64-bit
|
|
range.*/
|
|
if(OP_UNLIKELY(_src_gp<0)&&OP_UNLIKELY(_src_gp>=-1-_delta))return OP_EINVAL;
|
|
if(OP_UNLIKELY(_src_gp>OP_INT64_MAX-_delta)){
|
|
/*Adding this amount to the granule position would overflow the positive
|
|
half of its 64-bit range.
|
|
Since signed overflow is undefined in C, do it in a way the compiler
|
|
isn't allowed to screw up.*/
|
|
_delta-=(opus_int32)(OP_INT64_MAX-_src_gp)+1;
|
|
_src_gp=OP_INT64_MIN;
|
|
}
|
|
}
|
|
else if(_delta<0){
|
|
/*Subtracting this amount from the granule position would underflow its
|
|
64-bit range.*/
|
|
if(_src_gp>=0&&OP_UNLIKELY(_src_gp<-_delta))return OP_EINVAL;
|
|
if(OP_UNLIKELY(_src_gp<OP_INT64_MIN-_delta)){
|
|
/*Subtracting this amount from the granule position would underflow the
|
|
negative half of its 64-bit range.
|
|
Since signed underflow is undefined in C, do it in a way the compiler
|
|
isn't allowed to screw up.*/
|
|
_delta+=(opus_int32)(_src_gp-OP_INT64_MIN)+1;
|
|
_src_gp=OP_INT64_MAX;
|
|
}
|
|
}
|
|
*_dst_gp=_src_gp+_delta;
|
|
return 0;
|
|
}
|
|
|
|
/*Safely computes the difference between two granule positions.
|
|
The difference must fit in a signed 64-bit integer, or the function fails.
|
|
It correctly handles the case where the granule position has wrapped around
|
|
from positive values to negative ones.
|
|
[out] _delta: The difference between the granule positions.
|
|
Only modified on success.
|
|
_gp_a: The granule position to subtract from.
|
|
This must not be -1.
|
|
_gp_b: The granule position to subtract.
|
|
This must not be -1.
|
|
Return: 0 on success, or OP_EINVAL if the result would not fit in a signed
|
|
64-bit integer.*/
|
|
static int op_granpos_diff(ogg_int64_t *_delta,
|
|
ogg_int64_t _gp_a,ogg_int64_t _gp_b){
|
|
int gp_a_negative;
|
|
int gp_b_negative;
|
|
/*The code below handles these cases correctly, but there's no reason we
|
|
should ever be called with these values, so make sure we aren't.*/
|
|
OP_ASSERT(_gp_a!=-1);
|
|
OP_ASSERT(_gp_b!=-1);
|
|
gp_a_negative=OP_UNLIKELY(_gp_a<0);
|
|
gp_b_negative=OP_UNLIKELY(_gp_b<0);
|
|
if(OP_UNLIKELY(gp_a_negative^gp_b_negative)){
|
|
ogg_int64_t da;
|
|
ogg_int64_t db;
|
|
if(gp_a_negative){
|
|
/*_gp_a has wrapped to a negative value but _gp_b hasn't: the difference
|
|
should be positive.*/
|
|
/*Step 1: Handle wrapping.*/
|
|
/*_gp_a < 0 => da < 0.*/
|
|
da=(OP_INT64_MIN-_gp_a)-1;
|
|
/*_gp_b >= 0 => db >= 0.*/
|
|
db=OP_INT64_MAX-_gp_b;
|
|
/*Step 2: Check for overflow.*/
|
|
if(OP_UNLIKELY(OP_INT64_MAX+da<db))return OP_EINVAL;
|
|
*_delta=db-da;
|
|
}
|
|
else{
|
|
/*_gp_b has wrapped to a negative value but _gp_a hasn't: the difference
|
|
should be negative.*/
|
|
/*Step 1: Handle wrapping.*/
|
|
/*_gp_a >= 0 => da <= 0*/
|
|
da=_gp_a+OP_INT64_MIN;
|
|
/*_gp_b < 0 => db <= 0*/
|
|
db=OP_INT64_MIN-_gp_b;
|
|
/*Step 2: Check for overflow.*/
|
|
if(OP_UNLIKELY(da<OP_INT64_MIN-db))return OP_EINVAL;
|
|
*_delta=da+db;
|
|
}
|
|
}
|
|
else *_delta=_gp_a-_gp_b;
|
|
return 0;
|
|
}
|
|
|
|
static int op_granpos_cmp(ogg_int64_t _gp_a,ogg_int64_t _gp_b){
|
|
/*The invalid granule position -1 should behave like NaN: neither greater
|
|
than nor less than any other granule position, nor equal to any other
|
|
granule position, including itself.
|
|
However, that means there isn't anything we could sensibly return from this
|
|
function for it.*/
|
|
OP_ASSERT(_gp_a!=-1);
|
|
OP_ASSERT(_gp_b!=-1);
|
|
/*Handle the wrapping cases.*/
|
|
if(OP_UNLIKELY(_gp_a<0)){
|
|
if(_gp_b>=0)return 1;
|
|
/*Else fall through.*/
|
|
}
|
|
else if(OP_UNLIKELY(_gp_b<0))return -1;
|
|
/*No wrapping case.*/
|
|
return (_gp_a>_gp_b)-(_gp_b>_gp_a);
|
|
}
|
|
|
|
/*Returns the duration of the packet (in samples at 48 kHz), or a negative
|
|
value on error.*/
|
|
static int op_get_packet_duration(const unsigned char *_data,int _len){
|
|
int nframes;
|
|
int frame_size;
|
|
int nsamples;
|
|
nframes=opus_packet_get_nb_frames(_data,_len);
|
|
if(OP_UNLIKELY(nframes<0))return OP_EBADPACKET;
|
|
frame_size=opus_packet_get_samples_per_frame(_data,48000);
|
|
nsamples=nframes*frame_size;
|
|
if(OP_UNLIKELY(nsamples>120*48))return OP_EBADPACKET;
|
|
return nsamples;
|
|
}
|
|
|
|
/*This function more properly belongs in info.c, but we define it here to allow
|
|
the static granule position manipulation functions to remain static.*/
|
|
ogg_int64_t opus_granule_sample(const OpusHead *_head,ogg_int64_t _gp){
|
|
opus_int32 pre_skip;
|
|
pre_skip=_head->pre_skip;
|
|
if(_gp!=-1&&op_granpos_add(&_gp,_gp,-pre_skip))_gp=-1;
|
|
return _gp;
|
|
}
|
|
|
|
/*Grab all the packets currently in the stream state, and compute their
|
|
durations.
|
|
_of->op_count is set to the number of packets collected.
|
|
[out] _durations: Returns the durations of the individual packets.
|
|
Return: The total duration of all packets, or OP_HOLE if there was a hole.*/
|
|
static opus_int32 op_collect_audio_packets(OggOpusFile *_of,
|
|
int _durations[255]){
|
|
opus_int32 total_duration;
|
|
int op_count;
|
|
/*Count the durations of all packets in the page.*/
|
|
op_count=0;
|
|
total_duration=0;
|
|
for(;;){
|
|
int ret;
|
|
/*This takes advantage of undocumented libogg behavior that returned
|
|
ogg_packet buffers are valid at least until the next page is
|
|
submitted.
|
|
Relying on this is not too terrible, as _none_ of the Ogg memory
|
|
ownership/lifetime rules are well-documented.
|
|
But I can read its code and know this will work.*/
|
|
ret=ogg_stream_packetout(&_of->os,_of->op+op_count);
|
|
if(!ret)break;
|
|
if(OP_UNLIKELY(ret<0)){
|
|
/*We shouldn't get holes in the middle of pages.*/
|
|
OP_ASSERT(op_count==0);
|
|
/*Set the return value and break out of the loop.
|
|
We want to make sure op_count gets set to 0, because we've ingested a
|
|
page, so any previously loaded packets are now invalid.*/
|
|
total_duration=OP_HOLE;
|
|
break;
|
|
}
|
|
/*Unless libogg is broken, we can't get more than 255 packets from a
|
|
single page.*/
|
|
OP_ASSERT(op_count<255);
|
|
_durations[op_count]=op_get_packet_duration(_of->op[op_count].packet,
|
|
_of->op[op_count].bytes);
|
|
if(OP_LIKELY(_durations[op_count]>0)){
|
|
/*With at most 255 packets on a page, this can't overflow.*/
|
|
total_duration+=_durations[op_count++];
|
|
}
|
|
/*Ignore packets with an invalid TOC sequence.*/
|
|
else if(op_count>0){
|
|
/*But save the granule position, if there was one.*/
|
|
_of->op[op_count-1].granulepos=_of->op[op_count].granulepos;
|
|
}
|
|
}
|
|
_of->op_pos=0;
|
|
_of->op_count=op_count;
|
|
return total_duration;
|
|
}
|
|
|
|
/*Starting from current cursor position, get the initial PCM offset of the next
|
|
page.
|
|
This also validates the granule position on the first page with a completed
|
|
audio data packet, as required by the spec.
|
|
If this link is completely empty (no pages with completed packets), then this
|
|
function sets pcm_start=pcm_end=0 and returns the BOS page of the next link
|
|
(if any).
|
|
In the seekable case, we initialize pcm_end=-1 before calling this function,
|
|
so that later we can detect that the link was empty before calling
|
|
op_find_final_pcm_offset().
|
|
[inout] _link: The link for which to find pcm_start.
|
|
[out] _og: Returns the BOS page of the next link if this link was empty.
|
|
In the unseekable case, we can then feed this to
|
|
op_fetch_headers() to start the next link.
|
|
The caller may pass NULL (e.g., for seekable streams), in
|
|
which case this page will be discarded.
|
|
Return: 0 on success, 1 if there is a buffered BOS page available, or a
|
|
negative value on unrecoverable error.*/
|
|
static int op_find_initial_pcm_offset(OggOpusFile *_of,
|
|
OggOpusLink *_link,ogg_page *_og){
|
|
ogg_page og;
|
|
opus_int64 page_offset;
|
|
ogg_int64_t pcm_start;
|
|
ogg_int64_t prev_packet_gp;
|
|
ogg_int64_t cur_page_gp;
|
|
ogg_uint32_t serialno;
|
|
opus_int32 total_duration;
|
|
int durations[255];
|
|
int cur_page_eos;
|
|
int op_count;
|
|
int pi;
|
|
if(_og==NULL)_og=&og;
|
|
serialno=_of->os.serialno;
|
|
op_count=0;
|
|
/*We shouldn't have to initialize total_duration, but gcc is too dumb to
|
|
figure out that op_count>0 implies we've been through the whole loop at
|
|
least once.*/
|
|
total_duration=0;
|
|
do{
|
|
page_offset=op_get_next_page(_of,_og,_of->end);
|
|
/*We should get a page unless the file is truncated or mangled.
|
|
Otherwise there are no audio data packets in the whole logical stream.*/
|
|
if(OP_UNLIKELY(page_offset<0)){
|
|
/*Fail if there was a read error.*/
|
|
if(page_offset<OP_FALSE)return (int)page_offset;
|
|
/*Fail if the pre-skip is non-zero, since it's asking us to skip more
|
|
samples than exist.*/
|
|
if(_link->head.pre_skip>0)return OP_EBADTIMESTAMP;
|
|
/*Set pcm_end and end_offset so we can skip the call to
|
|
op_find_final_pcm_offset().*/
|
|
_link->pcm_start=_link->pcm_end=0;
|
|
_link->end_offset=_link->data_offset;
|
|
return 0;
|
|
}
|
|
/*Similarly, if we hit the next link in the chain, we've gone too far.*/
|
|
if(OP_UNLIKELY(ogg_page_bos(_og))){
|
|
if(_link->head.pre_skip>0)return OP_EBADTIMESTAMP;
|
|
/*Set pcm_end and end_offset so we can skip the call to
|
|
op_find_final_pcm_offset().*/
|
|
_link->pcm_end=_link->pcm_start=0;
|
|
_link->end_offset=_link->data_offset;
|
|
/*Tell the caller we've got a buffered page for them.*/
|
|
return 1;
|
|
}
|
|
/*Ignore pages from other streams (not strictly necessary, because of the
|
|
checks in ogg_stream_pagein(), but saves some work).*/
|
|
if(serialno!=(ogg_uint32_t)ogg_page_serialno(_og))continue;
|
|
ogg_stream_pagein(&_of->os,_og);
|
|
/*Bitrate tracking: add the header's bytes here.
|
|
The body bytes are counted when we consume the packets.*/
|
|
_of->bytes_tracked+=_og->header_len;
|
|
/*Count the durations of all packets in the page.*/
|
|
do total_duration=op_collect_audio_packets(_of,durations);
|
|
/*Ignore holes.*/
|
|
while(OP_UNLIKELY(total_duration<0));
|
|
op_count=_of->op_count;
|
|
}
|
|
while(op_count<=0);
|
|
/*We found the first page with a completed audio data packet: actually look
|
|
at the granule position.
|
|
RFC 3533 says, "A special value of -1 (in two's complement) indicates that
|
|
no packets finish on this page," which does not say that a granule
|
|
position that is NOT -1 indicates that some packets DO finish on that page
|
|
(even though this was the intention, libogg itself violated this intention
|
|
for years before we fixed it).
|
|
The Ogg Opus specification only imposes its start-time requirements
|
|
on the granule position of the first page with completed packets,
|
|
so we ignore any set granule positions until then.*/
|
|
cur_page_gp=_of->op[op_count-1].granulepos;
|
|
/*But getting a packet without a valid granule position on the page is not
|
|
okay.*/
|
|
if(cur_page_gp==-1)return OP_EBADTIMESTAMP;
|
|
cur_page_eos=_of->op[op_count-1].e_o_s;
|
|
if(OP_LIKELY(!cur_page_eos)){
|
|
/*The EOS flag wasn't set.
|
|
Work backwards from the provided granule position to get the starting PCM
|
|
offset.*/
|
|
if(OP_UNLIKELY(op_granpos_add(&pcm_start,cur_page_gp,-total_duration)<0)){
|
|
/*The starting granule position MUST not be smaller than the amount of
|
|
audio on the first page with completed packets.*/
|
|
return OP_EBADTIMESTAMP;
|
|
}
|
|
}
|
|
else{
|
|
/*The first page with completed packets was also the last.*/
|
|
if(OP_LIKELY(op_granpos_add(&pcm_start,cur_page_gp,-total_duration)<0)){
|
|
/*If there's less audio on the page than indicated by the granule
|
|
position, then we're doing end-trimming, and the starting PCM offset
|
|
is zero by spec mandate.*/
|
|
pcm_start=0;
|
|
/*However, the end-trimming MUST not ask us to trim more samples than
|
|
exist after applying the pre-skip.*/
|
|
if(OP_UNLIKELY(op_granpos_cmp(cur_page_gp,_link->head.pre_skip)<0)){
|
|
return OP_EBADTIMESTAMP;
|
|
}
|
|
}
|
|
}
|
|
/*Timestamp the individual packets.*/
|
|
prev_packet_gp=pcm_start;
|
|
for(pi=0;pi<op_count;pi++){
|
|
if(cur_page_eos){
|
|
ogg_int64_t diff;
|
|
OP_ALWAYS_TRUE(!op_granpos_diff(&diff,cur_page_gp,prev_packet_gp));
|
|
diff=durations[pi]-diff;
|
|
/*If we have samples to trim...*/
|
|
if(diff>0){
|
|
/*If we trimmed the entire packet, stop (the spec says encoders
|
|
shouldn't do this, but we support it anyway).*/
|
|
if(OP_UNLIKELY(diff>durations[pi]))break;
|
|
_of->op[pi].granulepos=prev_packet_gp=cur_page_gp;
|
|
/*Move the EOS flag to this packet, if necessary, so we'll trim the
|
|
samples.*/
|
|
_of->op[pi].e_o_s=1;
|
|
continue;
|
|
}
|
|
}
|
|
/*Update the granule position as normal.*/
|
|
OP_ALWAYS_TRUE(!op_granpos_add(&_of->op[pi].granulepos,
|
|
prev_packet_gp,durations[pi]));
|
|
prev_packet_gp=_of->op[pi].granulepos;
|
|
}
|
|
/*Update the packet count after end-trimming.*/
|
|
_of->op_count=pi;
|
|
_of->cur_discard_count=_link->head.pre_skip;
|
|
_of->prev_packet_gp=_link->pcm_start=pcm_start;
|
|
_of->prev_page_offset=page_offset;
|
|
return 0;
|
|
}
|
|
|
|
/*Starting from current cursor position, get the final PCM offset of the
|
|
previous page.
|
|
This also validates the duration of the link, which, while not strictly
|
|
required by the spec, we need to ensure duration calculations don't
|
|
overflow.
|
|
This is only done for seekable sources.
|
|
We must validate that op_find_initial_pcm_offset() succeeded for this link
|
|
before calling this function, otherwise it will scan the entire stream
|
|
backwards until it reaches the start, and then fail.*/
|
|
static int op_find_final_pcm_offset(OggOpusFile *_of,
|
|
const ogg_uint32_t *_serialnos,int _nserialnos,OggOpusLink *_link,
|
|
opus_int64 _offset,ogg_uint32_t _end_serialno,ogg_int64_t _end_gp,
|
|
ogg_int64_t *_total_duration){
|
|
ogg_int64_t total_duration;
|
|
ogg_int64_t duration;
|
|
ogg_uint32_t cur_serialno;
|
|
/*For the time being, fetch end PCM offset the simple way.*/
|
|
cur_serialno=_link->serialno;
|
|
if(_end_serialno!=cur_serialno||_end_gp==-1){
|
|
_offset=op_get_last_page(_of,&_end_gp,_offset,
|
|
cur_serialno,_serialnos,_nserialnos);
|
|
if(OP_UNLIKELY(_offset<0))return (int)_offset;
|
|
}
|
|
/*At worst we should have found the first page with completed packets.*/
|
|
if(OP_UNLIKELY(_offset<_link->data_offset))return OP_EBADLINK;
|
|
/*This implementation requires that the difference between the first and last
|
|
granule positions in each link be representable in a signed, 64-bit
|
|
number, and that each link also have at least as many samples as the
|
|
pre-skip requires.*/
|
|
if(OP_UNLIKELY(op_granpos_diff(&duration,_end_gp,_link->pcm_start)<0)
|
|
||OP_UNLIKELY(duration<_link->head.pre_skip)){
|
|
return OP_EBADTIMESTAMP;
|
|
}
|
|
/*We also require that the total duration be representable in a signed,
|
|
64-bit number.*/
|
|
duration-=_link->head.pre_skip;
|
|
total_duration=*_total_duration;
|
|
if(OP_UNLIKELY(OP_INT64_MAX-duration<total_duration))return OP_EBADTIMESTAMP;
|
|
*_total_duration=total_duration+duration;
|
|
_link->pcm_end=_end_gp;
|
|
_link->end_offset=_offset;
|
|
return 0;
|
|
}
|
|
|
|
/*Rescale the number _x from the range [0,_from] to [0,_to].
|
|
_from and _to must be positive.*/
|
|
static opus_int64 op_rescale64(opus_int64 _x,opus_int64 _from,opus_int64 _to){
|
|
opus_int64 frac;
|
|
opus_int64 ret;
|
|
int i;
|
|
if(_x>=_from)return _to;
|
|
if(_x<=0)return 0;
|
|
frac=0;
|
|
for(i=0;i<63;i++){
|
|
frac<<=1;
|
|
OP_ASSERT(_x<=_from);
|
|
if(_x>=_from>>1){
|
|
_x-=_from-_x;
|
|
frac|=1;
|
|
}
|
|
else _x<<=1;
|
|
}
|
|
ret=0;
|
|
for(i=0;i<63;i++){
|
|
if(frac&1)ret=(ret&_to&1)+(ret>>1)+(_to>>1);
|
|
else ret>>=1;
|
|
frac>>=1;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/*The minimum granule position spacing allowed for making predictions.
|
|
This corresponds to about 1 second of audio at 48 kHz for both Opus and
|
|
Vorbis, or one keyframe interval in Theora with the default keyframe spacing
|
|
of 256.*/
|
|
#define OP_GP_SPACING_MIN (48000)
|
|
|
|
/*Try to estimate the location of the next link using the current seek
|
|
records, assuming the initial granule position of any streams we've found is
|
|
0.*/
|
|
static opus_int64 op_predict_link_start(const OpusSeekRecord *_sr,int _nsr,
|
|
opus_int64 _searched,opus_int64 _end_searched,opus_int32 _bias){
|
|
opus_int64 bisect;
|
|
int sri;
|
|
int srj;
|
|
/*Require that we be at least OP_CHUNK_SIZE from the end.
|
|
We don't require that we be at least OP_CHUNK_SIZE from the beginning,
|
|
because if we are we'll just scan forward without seeking.*/
|
|
_end_searched-=OP_CHUNK_SIZE;
|
|
if(_searched>=_end_searched)return -1;
|
|
bisect=_end_searched;
|
|
for(sri=0;sri<_nsr;sri++){
|
|
ogg_int64_t gp1;
|
|
ogg_int64_t gp2_min;
|
|
ogg_uint32_t serialno1;
|
|
opus_int64 offset1;
|
|
/*If the granule position is negative, either it's invalid or we'd cause
|
|
overflow.*/
|
|
gp1=_sr[sri].gp;
|
|
if(gp1<0)continue;
|
|
/*We require some minimum distance between granule positions to make an
|
|
estimate.
|
|
We don't actually know what granule position scheme is being used,
|
|
because we have no idea what kind of stream these came from.
|
|
Therefore we require a minimum spacing between them, with the
|
|
expectation that while bitrates and granule position increments might
|
|
vary locally in quite complex ways, they are globally smooth.*/
|
|
if(OP_UNLIKELY(op_granpos_add(&gp2_min,gp1,OP_GP_SPACING_MIN)<0)){
|
|
/*No granule position would satisfy us.*/
|
|
continue;
|
|
}
|
|
offset1=_sr[sri].offset;
|
|
serialno1=_sr[sri].serialno;
|
|
for(srj=sri;srj-->0;){
|
|
ogg_int64_t gp2;
|
|
opus_int64 offset2;
|
|
opus_int64 num;
|
|
ogg_int64_t den;
|
|
ogg_int64_t ipart;
|
|
gp2=_sr[srj].gp;
|
|
if(gp2<gp2_min)continue;
|
|
/*Oh, and also make sure these came from the same stream.*/
|
|
if(_sr[srj].serialno!=serialno1)continue;
|
|
offset2=_sr[srj].offset;
|
|
/*For once, we can subtract with impunity.*/
|
|
den=gp2-gp1;
|
|
ipart=gp2/den;
|
|
num=offset2-offset1;
|
|
OP_ASSERT(num>0);
|
|
if(ipart>0&&(offset2-_searched)/ipart<num)continue;
|
|
offset2-=ipart*num;
|
|
gp2-=ipart*den;
|
|
offset2-=op_rescale64(gp2,den,num)-_bias;
|
|
if(offset2<_searched)continue;
|
|
bisect=OP_MIN(bisect,offset2);
|
|
break;
|
|
}
|
|
}
|
|
return bisect>=_end_searched?-1:bisect;
|
|
}
|
|
|
|
/*Finds each bitstream link, one at a time, using a bisection search.
|
|
This has to begin by knowing the offset of the first link's initial page.*/
|
|
static int op_bisect_forward_serialno(OggOpusFile *_of,
|
|
opus_int64 _searched,OpusSeekRecord *_sr,int _csr,
|
|
ogg_uint32_t **_serialnos,int *_nserialnos,int *_cserialnos){
|
|
ogg_page og;
|
|
OggOpusLink *links;
|
|
int nlinks;
|
|
int clinks;
|
|
ogg_uint32_t *serialnos;
|
|
int nserialnos;
|
|
ogg_int64_t total_duration;
|
|
int nsr;
|
|
int ret;
|
|
links=_of->links;
|
|
nlinks=clinks=_of->nlinks;
|
|
total_duration=0;
|
|
/*We start with one seek record, for the last page in the file.
|
|
We build up a list of records for places we seek to during link
|
|
enumeration.
|
|
This list is kept sorted in reverse order.
|
|
We only care about seek locations that were _not_ in the current link,
|
|
therefore we can add them one at a time to the end of the list as we
|
|
improve the lower bound on the location where the next link starts.*/
|
|
nsr=1;
|
|
for(;;){
|
|
opus_int64 end_searched;
|
|
opus_int64 bisect;
|
|
opus_int64 next;
|
|
opus_int64 last;
|
|
ogg_int64_t end_offset;
|
|
ogg_int64_t end_gp;
|
|
int sri;
|
|
serialnos=*_serialnos;
|
|
nserialnos=*_nserialnos;
|
|
if(OP_UNLIKELY(nlinks>=clinks)){
|
|
if(OP_UNLIKELY(clinks>INT_MAX-1>>1))return OP_EFAULT;
|
|
clinks=2*clinks+1;
|
|
OP_ASSERT(nlinks<clinks);
|
|
links=(OggOpusLink *)_ogg_realloc(links,sizeof(*links)*clinks);
|
|
if(OP_UNLIKELY(links==NULL))return OP_EFAULT;
|
|
_of->links=links;
|
|
}
|
|
/*Invariants:
|
|
We have the headers and serial numbers for the link beginning at 'begin'.
|
|
We have the offset and granule position of the last page in the file
|
|
(potentially not a page we care about).*/
|
|
/*Scan the seek records we already have to save us some bisection.*/
|
|
for(sri=0;sri<nsr;sri++){
|
|
if(op_lookup_serialno(_sr[sri].serialno,serialnos,nserialnos))break;
|
|
}
|
|
/*Is the last page in our current list of serial numbers?*/
|
|
if(sri<=0)break;
|
|
/*Last page wasn't found.
|
|
We have at least one more link.*/
|
|
last=-1;
|
|
end_searched=_sr[sri-1].search_start;
|
|
next=_sr[sri-1].offset;
|
|
end_gp=-1;
|
|
if(sri<nsr){
|
|
_searched=_sr[sri].offset+_sr[sri].size;
|
|
if(_sr[sri].serialno==links[nlinks-1].serialno){
|
|
end_gp=_sr[sri].gp;
|
|
end_offset=_sr[sri].offset;
|
|
}
|
|
}
|
|
nsr=sri;
|
|
bisect=-1;
|
|
/*If we've already found the end of at least one link, try to pick the
|
|
first bisection point at twice the average link size.
|
|
This is a good choice for files with lots of links that are all about the
|
|
same size.*/
|
|
if(nlinks>1){
|
|
opus_int64 last_offset;
|
|
opus_int64 avg_link_size;
|
|
opus_int64 upper_limit;
|
|
last_offset=links[nlinks-1].offset;
|
|
avg_link_size=last_offset/(nlinks-1);
|
|
upper_limit=end_searched-OP_CHUNK_SIZE-avg_link_size;
|
|
if(OP_LIKELY(last_offset>_searched-avg_link_size)
|
|
&&OP_LIKELY(last_offset<upper_limit)){
|
|
bisect=last_offset+avg_link_size;
|
|
if(OP_LIKELY(bisect<upper_limit))bisect+=avg_link_size;
|
|
}
|
|
}
|
|
/*We guard against garbage separating the last and first pages of two
|
|
links below.*/
|
|
while(_searched<end_searched){
|
|
opus_int32 next_bias;
|
|
/*If we don't have a better estimate, use simple bisection.*/
|
|
if(bisect==-1)bisect=_searched+(end_searched-_searched>>1);
|
|
/*If we're within OP_CHUNK_SIZE of the start, scan forward.*/
|
|
if(bisect-_searched<OP_CHUNK_SIZE)bisect=_searched;
|
|
/*Otherwise we're skipping data.
|
|
Forget the end page, if we saw one, as we might miss a later one.*/
|
|
else end_gp=-1;
|
|
ret=op_seek_helper(_of,bisect);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
last=op_get_next_page(_of,&og,_sr[nsr-1].offset);
|
|
if(OP_UNLIKELY(last<OP_FALSE))return (int)last;
|
|
next_bias=0;
|
|
if(last==OP_FALSE)end_searched=bisect;
|
|
else{
|
|
ogg_uint32_t serialno;
|
|
ogg_int64_t gp;
|
|
serialno=ogg_page_serialno(&og);
|
|
gp=ogg_page_granulepos(&og);
|
|
if(!op_lookup_serialno(serialno,serialnos,nserialnos)){
|
|
end_searched=bisect;
|
|
next=last;
|
|
/*In reality we should always have enough room, but be paranoid.*/
|
|
if(OP_LIKELY(nsr<_csr)){
|
|
_sr[nsr].search_start=bisect;
|
|
_sr[nsr].offset=last;
|
|
OP_ASSERT(_of->offset-last>=0);
|
|
OP_ASSERT(_of->offset-last<=OP_PAGE_SIZE_MAX);
|
|
_sr[nsr].size=(opus_int32)(_of->offset-last);
|
|
_sr[nsr].serialno=serialno;
|
|
_sr[nsr].gp=gp;
|
|
nsr++;
|
|
}
|
|
}
|
|
else{
|
|
_searched=_of->offset;
|
|
next_bias=OP_CHUNK_SIZE;
|
|
if(serialno==links[nlinks-1].serialno){
|
|
/*This page was from the stream we want, remember it.
|
|
If it's the last such page in the link, we won't have to go back
|
|
looking for it later.*/
|
|
end_gp=gp;
|
|
end_offset=last;
|
|
}
|
|
}
|
|
}
|
|
bisect=op_predict_link_start(_sr,nsr,_searched,end_searched,next_bias);
|
|
}
|
|
/*Bisection point found.
|
|
Get the final granule position of the previous link, assuming
|
|
op_find_initial_pcm_offset() didn't already determine the link was
|
|
empty.*/
|
|
if(OP_LIKELY(links[nlinks-1].pcm_end==-1)){
|
|
if(end_gp==-1){
|
|
/*If we don't know where the end page is, we'll have to seek back and
|
|
look for it, starting from the end of the link.*/
|
|
end_offset=next;
|
|
/*Also forget the last page we read.
|
|
It won't be available after the seek.*/
|
|
last=-1;
|
|
}
|
|
ret=op_find_final_pcm_offset(_of,serialnos,nserialnos,
|
|
links+nlinks-1,end_offset,links[nlinks-1].serialno,end_gp,
|
|
&total_duration);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
}
|
|
if(last!=next){
|
|
/*The last page we read was not the first page the next link.
|
|
Move the cursor position to the offset of that first page.
|
|
This only performs an actual seek if the first page of the next link
|
|
does not start at the end of the last page from the current Opus
|
|
stream with a valid granule position.*/
|
|
ret=op_seek_helper(_of,next);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
}
|
|
ret=op_fetch_headers(_of,&links[nlinks].head,&links[nlinks].tags,
|
|
_serialnos,_nserialnos,_cserialnos,last!=next?NULL:&og);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
links[nlinks].offset=next;
|
|
links[nlinks].data_offset=_of->offset;
|
|
links[nlinks].serialno=_of->os.serialno;
|
|
links[nlinks].pcm_end=-1;
|
|
/*This might consume a page from the next link, however the next bisection
|
|
always starts with a seek.*/
|
|
ret=op_find_initial_pcm_offset(_of,links+nlinks,NULL);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
_searched=_of->offset;
|
|
/*Mark the current link count so it can be cleaned up on error.*/
|
|
_of->nlinks=++nlinks;
|
|
}
|
|
/*Last page is in the starting serialno list, so we've reached the last link.
|
|
Now find the last granule position for it (if we didn't the first time we
|
|
looked at the end of the stream, and if op_find_initial_pcm_offset()
|
|
didn't already determine the link was empty).*/
|
|
if(OP_LIKELY(links[nlinks-1].pcm_end==-1)){
|
|
ret=op_find_final_pcm_offset(_of,serialnos,nserialnos,
|
|
links+nlinks-1,_sr[0].offset,_sr[0].serialno,_sr[0].gp,&total_duration);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
}
|
|
/*Trim back the links array if necessary.*/
|
|
links=(OggOpusLink *)_ogg_realloc(links,sizeof(*links)*nlinks);
|
|
if(OP_LIKELY(links!=NULL))_of->links=links;
|
|
/*We also don't need these anymore.*/
|
|
_ogg_free(*_serialnos);
|
|
*_serialnos=NULL;
|
|
*_cserialnos=*_nserialnos=0;
|
|
return 0;
|
|
}
|
|
|
|
static void op_update_gain(OggOpusFile *_of){
|
|
OpusHead *head;
|
|
opus_int32 gain_q8;
|
|
int li;
|
|
/*If decode isn't ready, then we'll apply the gain when we initialize the
|
|
decoder.*/
|
|
if(_of->ready_state<OP_INITSET)return;
|
|
gain_q8=_of->gain_offset_q8;
|
|
li=_of->seekable?_of->cur_link:0;
|
|
head=&_of->links[li].head;
|
|
/*We don't have to worry about overflow here because the header gain and
|
|
track gain must lie in the range [-32768,32767], and the user-supplied
|
|
offset has been pre-clamped to [-98302,98303].*/
|
|
switch(_of->gain_type){
|
|
case OP_ALBUM_GAIN:{
|
|
int album_gain_q8;
|
|
album_gain_q8=0;
|
|
opus_tags_get_album_gain(&_of->links[li].tags,&album_gain_q8);
|
|
gain_q8+=album_gain_q8;
|
|
gain_q8+=head->output_gain;
|
|
}break;
|
|
case OP_TRACK_GAIN:{
|
|
int track_gain_q8;
|
|
track_gain_q8=0;
|
|
opus_tags_get_track_gain(&_of->links[li].tags,&track_gain_q8);
|
|
gain_q8+=track_gain_q8;
|
|
gain_q8+=head->output_gain;
|
|
}break;
|
|
case OP_HEADER_GAIN:gain_q8+=head->output_gain;break;
|
|
case OP_ABSOLUTE_GAIN:break;
|
|
default:OP_ASSERT(0);
|
|
}
|
|
gain_q8=OP_CLAMP(-32768,gain_q8,32767);
|
|
OP_ASSERT(_of->od!=NULL);
|
|
#if defined(OPUS_SET_GAIN)
|
|
opus_multistream_decoder_ctl(_of->od,OPUS_SET_GAIN(gain_q8));
|
|
#else
|
|
/*A fallback that works with both float and fixed-point is a bunch of work,
|
|
so just force people to use a sufficiently new version.
|
|
This is deployed well enough at this point that this shouldn't be a burden.*/
|
|
# error "libopus 1.0.1 or later required"
|
|
#endif
|
|
}
|
|
|
|
static int op_make_decode_ready(OggOpusFile *_of){
|
|
const OpusHead *head;
|
|
int li;
|
|
int stream_count;
|
|
int coupled_count;
|
|
int channel_count;
|
|
if(_of->ready_state>OP_STREAMSET)return 0;
|
|
if(OP_UNLIKELY(_of->ready_state<OP_STREAMSET))return OP_EFAULT;
|
|
li=_of->seekable?_of->cur_link:0;
|
|
head=&_of->links[li].head;
|
|
stream_count=head->stream_count;
|
|
coupled_count=head->coupled_count;
|
|
channel_count=head->channel_count;
|
|
/*Check to see if the current decoder is compatible with the current link.*/
|
|
if(_of->od!=NULL&&_of->od_stream_count==stream_count
|
|
&&_of->od_coupled_count==coupled_count&&_of->od_channel_count==channel_count
|
|
&&memcmp(_of->od_mapping,head->mapping,
|
|
sizeof(*head->mapping)*channel_count)==0){
|
|
opus_multistream_decoder_ctl(_of->od,OPUS_RESET_STATE);
|
|
}
|
|
else{
|
|
int err;
|
|
opus_multistream_decoder_destroy(_of->od);
|
|
_of->od=opus_multistream_decoder_create(48000,channel_count,
|
|
stream_count,coupled_count,head->mapping,&err);
|
|
if(_of->od==NULL)return OP_EFAULT;
|
|
_of->od_stream_count=stream_count;
|
|
_of->od_coupled_count=coupled_count;
|
|
_of->od_channel_count=channel_count;
|
|
memcpy(_of->od_mapping,head->mapping,sizeof(*head->mapping)*channel_count);
|
|
}
|
|
_of->ready_state=OP_INITSET;
|
|
_of->bytes_tracked=0;
|
|
_of->samples_tracked=0;
|
|
#if !defined(OP_FIXED_POINT)
|
|
_of->state_channel_count=0;
|
|
/*Use the serial number for the PRNG seed to get repeatable output for
|
|
straight play-throughs.*/
|
|
_of->dither_seed=_of->links[li].serialno;
|
|
#endif
|
|
op_update_gain(_of);
|
|
return 0;
|
|
}
|
|
|
|
static int op_open_seekable2_impl(OggOpusFile *_of){
|
|
/*64 seek records should be enough for anybody.
|
|
Actually, with a bisection search in a 63-bit range down to OP_CHUNK_SIZE
|
|
granularity, much more than enough.*/
|
|
OpusSeekRecord sr[64];
|
|
opus_int64 data_offset;
|
|
int ret;
|
|
/*We can seek, so set out learning all about this file.*/
|
|
(*_of->callbacks.seek)(_of->source,0,SEEK_END);
|
|
_of->offset=_of->end=(*_of->callbacks.tell)(_of->source);
|
|
if(OP_UNLIKELY(_of->end<0))return OP_EREAD;
|
|
data_offset=_of->links[0].data_offset;
|
|
if(OP_UNLIKELY(_of->end<data_offset))return OP_EBADLINK;
|
|
/*Get the offset of the last page of the physical bitstream, or, if we're
|
|
lucky, the last Opus page of the first link, as most Ogg Opus files will
|
|
contain a single logical bitstream.*/
|
|
ret=op_get_prev_page_serial(_of,sr,_of->end,
|
|
_of->links[0].serialno,_of->serialnos,_of->nserialnos);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
/*If there's any trailing junk, forget about it.*/
|
|
_of->end=sr[0].offset+sr[0].size;
|
|
if(OP_UNLIKELY(_of->end<data_offset))return OP_EBADLINK;
|
|
/*Now enumerate the bitstream structure.*/
|
|
return op_bisect_forward_serialno(_of,data_offset,sr,sizeof(sr)/sizeof(*sr),
|
|
&_of->serialnos,&_of->nserialnos,&_of->cserialnos);
|
|
}
|
|
|
|
static int op_open_seekable2(OggOpusFile *_of){
|
|
ogg_sync_state oy_start;
|
|
ogg_stream_state os_start;
|
|
ogg_packet *op_start;
|
|
opus_int64 prev_page_offset;
|
|
opus_int64 start_offset;
|
|
int start_op_count;
|
|
int ret;
|
|
/*We're partially open and have a first link header state in storage in _of.
|
|
Save off that stream state so we can come back to it.
|
|
It would be simpler to just dump all this state and seek back to
|
|
links[0].data_offset when we're done.
|
|
But we do the extra work to allow us to seek back to _exactly_ the same
|
|
stream position we're at now.
|
|
This allows, e.g., the HTTP backend to continue reading from the original
|
|
connection (if it's still available), instead of opening a new one.
|
|
This means we can open and start playing a normal Opus file with a single
|
|
link and reasonable packet sizes using only two HTTP requests.*/
|
|
start_op_count=_of->op_count;
|
|
/*This is a bit too large to put on the stack unconditionally.*/
|
|
op_start=(ogg_packet *)_ogg_malloc(sizeof(*op_start)*start_op_count);
|
|
if(op_start==NULL)return OP_EFAULT;
|
|
*&oy_start=_of->oy;
|
|
*&os_start=_of->os;
|
|
prev_page_offset=_of->prev_page_offset;
|
|
start_offset=_of->offset;
|
|
memcpy(op_start,_of->op,sizeof(*op_start)*start_op_count);
|
|
OP_ASSERT((*_of->callbacks.tell)(_of->source)==op_position(_of));
|
|
ogg_sync_init(&_of->oy);
|
|
ogg_stream_init(&_of->os,-1);
|
|
ret=op_open_seekable2_impl(_of);
|
|
/*Restore the old stream state.*/
|
|
ogg_stream_clear(&_of->os);
|
|
ogg_sync_clear(&_of->oy);
|
|
*&_of->oy=*&oy_start;
|
|
*&_of->os=*&os_start;
|
|
_of->offset=start_offset;
|
|
_of->op_count=start_op_count;
|
|
memcpy(_of->op,op_start,sizeof(*_of->op)*start_op_count);
|
|
_ogg_free(op_start);
|
|
_of->prev_packet_gp=_of->links[0].pcm_start;
|
|
_of->prev_page_offset=prev_page_offset;
|
|
_of->cur_discard_count=_of->links[0].head.pre_skip;
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
/*And restore the position indicator.*/
|
|
ret=(*_of->callbacks.seek)(_of->source,op_position(_of),SEEK_SET);
|
|
return OP_UNLIKELY(ret<0)?OP_EREAD:0;
|
|
}
|
|
|
|
/*Clear out the current logical bitstream decoder.*/
|
|
static void op_decode_clear(OggOpusFile *_of){
|
|
/*We don't actually free the decoder.
|
|
We might be able to re-use it for the next link.*/
|
|
_of->op_count=0;
|
|
_of->od_buffer_size=0;
|
|
_of->prev_packet_gp=-1;
|
|
_of->prev_page_offset=-1;
|
|
if(!_of->seekable){
|
|
OP_ASSERT(_of->ready_state>=OP_INITSET);
|
|
opus_tags_clear(&_of->links[0].tags);
|
|
}
|
|
_of->ready_state=OP_OPENED;
|
|
}
|
|
|
|
static void op_clear(OggOpusFile *_of){
|
|
OggOpusLink *links;
|
|
_ogg_free(_of->od_buffer);
|
|
if(_of->od!=NULL)opus_multistream_decoder_destroy(_of->od);
|
|
links=_of->links;
|
|
if(!_of->seekable){
|
|
if(_of->ready_state>OP_OPENED||_of->ready_state==OP_PARTOPEN){
|
|
opus_tags_clear(&links[0].tags);
|
|
}
|
|
}
|
|
else if(OP_LIKELY(links!=NULL)){
|
|
int nlinks;
|
|
int link;
|
|
nlinks=_of->nlinks;
|
|
for(link=0;link<nlinks;link++)opus_tags_clear(&links[link].tags);
|
|
}
|
|
_ogg_free(links);
|
|
_ogg_free(_of->serialnos);
|
|
ogg_stream_clear(&_of->os);
|
|
ogg_sync_clear(&_of->oy);
|
|
if(_of->callbacks.close!=NULL)(*_of->callbacks.close)(_of->source);
|
|
}
|
|
|
|
static int op_open1(OggOpusFile *_of,
|
|
void *_source,const OpusFileCallbacks *_cb,
|
|
const unsigned char *_initial_data,size_t _initial_bytes){
|
|
ogg_page og;
|
|
ogg_page *pog;
|
|
int seekable;
|
|
int ret;
|
|
memset(_of,0,sizeof(*_of));
|
|
_of->end=-1;
|
|
_of->source=_source;
|
|
*&_of->callbacks=*_cb;
|
|
/*At a minimum, we need to be able to read data.*/
|
|
if(OP_UNLIKELY(_of->callbacks.read==NULL))return OP_EREAD;
|
|
/*Initialize the framing state.*/
|
|
ogg_sync_init(&_of->oy);
|
|
/*Perhaps some data was previously read into a buffer for testing against
|
|
other stream types.
|
|
Allow initialization from this previously read data (especially as we may
|
|
be reading from a non-seekable stream).
|
|
This requires copying it into a buffer allocated by ogg_sync_buffer() and
|
|
doesn't support seeking, so this is not a good mechanism to use for
|
|
decoding entire files from RAM.*/
|
|
if(_initial_bytes>0){
|
|
char *buffer;
|
|
buffer=ogg_sync_buffer(&_of->oy,_initial_bytes);
|
|
memcpy(buffer,_initial_data,_initial_bytes*sizeof(*buffer));
|
|
ogg_sync_wrote(&_of->oy,_initial_bytes);
|
|
}
|
|
/*Can we seek?
|
|
Stevens suggests the seek test is portable.*/
|
|
seekable=_cb->seek!=NULL&&(*_cb->seek)(_source,0,SEEK_CUR)!=-1;
|
|
/*If seek is implemented, tell must also be implemented.*/
|
|
if(seekable){
|
|
opus_int64 pos;
|
|
if(OP_UNLIKELY(_of->callbacks.tell==NULL))return OP_EINVAL;
|
|
pos=(*_of->callbacks.tell)(_of->source);
|
|
/*If the current position is not equal to the initial bytes consumed,
|
|
absolute seeking will not work.*/
|
|
if(OP_UNLIKELY(pos!=(opus_int64)_initial_bytes))return OP_EINVAL;
|
|
}
|
|
_of->seekable=seekable;
|
|
/*Don't seek yet.
|
|
Set up a 'single' (current) logical bitstream entry for partial open.*/
|
|
_of->links=(OggOpusLink *)_ogg_malloc(sizeof(*_of->links));
|
|
/*The serialno gets filled in later by op_fetch_headers().*/
|
|
ogg_stream_init(&_of->os,-1);
|
|
pog=NULL;
|
|
for(;;){
|
|
/*Fetch all BOS pages, store the Opus header and all seen serial numbers,
|
|
and load subsequent Opus setup headers.*/
|
|
ret=op_fetch_headers(_of,&_of->links[0].head,&_of->links[0].tags,
|
|
&_of->serialnos,&_of->nserialnos,&_of->cserialnos,pog);
|
|
if(OP_UNLIKELY(ret<0))break;
|
|
_of->nlinks=1;
|
|
_of->links[0].offset=0;
|
|
_of->links[0].data_offset=_of->offset;
|
|
_of->links[0].pcm_end=-1;
|
|
_of->links[0].serialno=_of->os.serialno;
|
|
/*Fetch the initial PCM offset.*/
|
|
ret=op_find_initial_pcm_offset(_of,_of->links,&og);
|
|
if(seekable||OP_LIKELY(ret<=0))break;
|
|
/*This link was empty, but we already have the BOS page for the next one in
|
|
og.
|
|
We can't seek, so start processing the next link right now.*/
|
|
opus_tags_clear(&_of->links[0].tags);
|
|
_of->nlinks=0;
|
|
if(!seekable)_of->cur_link++;
|
|
pog=&og;
|
|
}
|
|
if(OP_LIKELY(ret>=0))_of->ready_state=OP_PARTOPEN;
|
|
return ret;
|
|
}
|
|
|
|
static int op_open2(OggOpusFile *_of){
|
|
int ret;
|
|
OP_ASSERT(_of->ready_state==OP_PARTOPEN);
|
|
if(_of->seekable){
|
|
_of->ready_state=OP_OPENED;
|
|
ret=op_open_seekable2(_of);
|
|
}
|
|
else ret=0;
|
|
if(OP_LIKELY(ret>=0)){
|
|
/*We have buffered packets from op_find_initial_pcm_offset().
|
|
Move to OP_INITSET so we can use them.*/
|
|
_of->ready_state=OP_STREAMSET;
|
|
ret=op_make_decode_ready(_of);
|
|
if(OP_LIKELY(ret>=0))return 0;
|
|
}
|
|
/*Don't auto-close the stream on failure.*/
|
|
_of->callbacks.close=NULL;
|
|
op_clear(_of);
|
|
return ret;
|
|
}
|
|
|
|
OggOpusFile *op_test_callbacks(void *_source,const OpusFileCallbacks *_cb,
|
|
const unsigned char *_initial_data,size_t _initial_bytes,int *_error){
|
|
OggOpusFile *of;
|
|
int ret;
|
|
of=(OggOpusFile *)_ogg_malloc(sizeof(*of));
|
|
ret=OP_EFAULT;
|
|
if(OP_LIKELY(of!=NULL)){
|
|
ret=op_open1(of,_source,_cb,_initial_data,_initial_bytes);
|
|
if(OP_LIKELY(ret>=0)){
|
|
if(_error!=NULL)*_error=0;
|
|
return of;
|
|
}
|
|
/*Don't auto-close the stream on failure.*/
|
|
of->callbacks.close=NULL;
|
|
op_clear(of);
|
|
_ogg_free(of);
|
|
}
|
|
if(_error!=NULL)*_error=ret;
|
|
return NULL;
|
|
}
|
|
|
|
OggOpusFile *op_open_callbacks(void *_source,const OpusFileCallbacks *_cb,
|
|
const unsigned char *_initial_data,size_t _initial_bytes,int *_error){
|
|
OggOpusFile *of;
|
|
of=op_test_callbacks(_source,_cb,_initial_data,_initial_bytes,_error);
|
|
if(OP_LIKELY(of!=NULL)){
|
|
int ret;
|
|
ret=op_open2(of);
|
|
if(OP_LIKELY(ret>=0))return of;
|
|
if(_error!=NULL)*_error=ret;
|
|
_ogg_free(of);
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/*Convenience routine to clean up from failure for the open functions that
|
|
create their own streams.*/
|
|
static OggOpusFile *op_open_close_on_failure(void *_source,
|
|
const OpusFileCallbacks *_cb,int *_error){
|
|
OggOpusFile *of;
|
|
if(OP_UNLIKELY(_source==NULL)){
|
|
if(_error!=NULL)*_error=OP_EFAULT;
|
|
return NULL;
|
|
}
|
|
of=op_open_callbacks(_source,_cb,NULL,0,_error);
|
|
if(OP_UNLIKELY(of==NULL))(*_cb->close)(_source);
|
|
return of;
|
|
}
|
|
|
|
OggOpusFile *op_open_file(const char *_path,int *_error){
|
|
OpusFileCallbacks cb;
|
|
return op_open_close_on_failure(op_fopen(&cb,_path,"rb"),&cb,_error);
|
|
}
|
|
|
|
OggOpusFile *op_open_memory(const unsigned char *_data,size_t _size,
|
|
int *_error){
|
|
OpusFileCallbacks cb;
|
|
return op_open_close_on_failure(op_mem_stream_create(&cb,_data,_size),&cb,
|
|
_error);
|
|
}
|
|
|
|
/*Convenience routine to clean up from failure for the open functions that
|
|
create their own streams.*/
|
|
static OggOpusFile *op_test_close_on_failure(void *_source,
|
|
const OpusFileCallbacks *_cb,int *_error){
|
|
OggOpusFile *of;
|
|
if(OP_UNLIKELY(_source==NULL)){
|
|
if(_error!=NULL)*_error=OP_EFAULT;
|
|
return NULL;
|
|
}
|
|
of=op_test_callbacks(_source,_cb,NULL,0,_error);
|
|
if(OP_UNLIKELY(of==NULL))(*_cb->close)(_source);
|
|
return of;
|
|
}
|
|
|
|
OggOpusFile *op_test_file(const char *_path,int *_error){
|
|
OpusFileCallbacks cb;
|
|
return op_test_close_on_failure(op_fopen(&cb,_path,"rb"),&cb,_error);
|
|
}
|
|
|
|
OggOpusFile *op_test_memory(const unsigned char *_data,size_t _size,
|
|
int *_error){
|
|
OpusFileCallbacks cb;
|
|
return op_test_close_on_failure(op_mem_stream_create(&cb,_data,_size),&cb,
|
|
_error);
|
|
}
|
|
|
|
int op_test_open(OggOpusFile *_of){
|
|
int ret;
|
|
if(OP_UNLIKELY(_of->ready_state!=OP_PARTOPEN))return OP_EINVAL;
|
|
ret=op_open2(_of);
|
|
/*op_open2() will clear this structure on failure.
|
|
Reset its contents to prevent double-frees in op_free().*/
|
|
if(OP_UNLIKELY(ret<0))memset(_of,0,sizeof(*_of));
|
|
return ret;
|
|
}
|
|
|
|
void op_free(OggOpusFile *_of){
|
|
if(OP_LIKELY(_of!=NULL)){
|
|
op_clear(_of);
|
|
_ogg_free(_of);
|
|
}
|
|
}
|
|
|
|
int op_seekable(const OggOpusFile *_of){
|
|
return _of->seekable;
|
|
}
|
|
|
|
int op_link_count(const OggOpusFile *_of){
|
|
return _of->nlinks;
|
|
}
|
|
|
|
ogg_uint32_t op_serialno(const OggOpusFile *_of,int _li){
|
|
if(OP_UNLIKELY(_li>=_of->nlinks))_li=_of->nlinks-1;
|
|
if(!_of->seekable)_li=0;
|
|
return _of->links[_li<0?_of->cur_link:_li].serialno;
|
|
}
|
|
|
|
int op_channel_count(const OggOpusFile *_of,int _li){
|
|
return op_head(_of,_li)->channel_count;
|
|
}
|
|
|
|
opus_int64 op_raw_total(const OggOpusFile *_of,int _li){
|
|
if(OP_UNLIKELY(_of->ready_state<OP_OPENED)
|
|
||OP_UNLIKELY(!_of->seekable)
|
|
||OP_UNLIKELY(_li>=_of->nlinks)){
|
|
return OP_EINVAL;
|
|
}
|
|
if(_li<0)return _of->end-_of->links[0].offset;
|
|
return (_li+1>=_of->nlinks?_of->end:_of->links[_li+1].offset)
|
|
-_of->links[_li].offset;
|
|
}
|
|
|
|
ogg_int64_t op_pcm_total(const OggOpusFile *_of,int _li){
|
|
OggOpusLink *links;
|
|
ogg_int64_t diff;
|
|
int nlinks;
|
|
nlinks=_of->nlinks;
|
|
if(OP_UNLIKELY(_of->ready_state<OP_OPENED)
|
|
||OP_UNLIKELY(!_of->seekable)
|
|
||OP_UNLIKELY(_li>=nlinks)){
|
|
return OP_EINVAL;
|
|
}
|
|
links=_of->links;
|
|
/*We verify that the granule position differences are larger than the
|
|
pre-skip and that the total duration does not overflow during link
|
|
enumeration, so we don't have to check here.*/
|
|
if(_li<0){
|
|
ogg_int64_t pcm_total;
|
|
int li;
|
|
pcm_total=0;
|
|
for(li=0;li<nlinks;li++){
|
|
OP_ALWAYS_TRUE(!op_granpos_diff(&diff,
|
|
links[li].pcm_end,links[li].pcm_start));
|
|
pcm_total+=diff-links[li].head.pre_skip;
|
|
}
|
|
return pcm_total;
|
|
}
|
|
OP_ALWAYS_TRUE(!op_granpos_diff(&diff,
|
|
links[_li].pcm_end,links[_li].pcm_start));
|
|
return diff-links[_li].head.pre_skip;
|
|
}
|
|
|
|
const OpusHead *op_head(const OggOpusFile *_of,int _li){
|
|
if(OP_UNLIKELY(_li>=_of->nlinks))_li=_of->nlinks-1;
|
|
if(!_of->seekable)_li=0;
|
|
return &_of->links[_li<0?_of->cur_link:_li].head;
|
|
}
|
|
|
|
const OpusTags *op_tags(const OggOpusFile *_of,int _li){
|
|
if(OP_UNLIKELY(_li>=_of->nlinks))_li=_of->nlinks-1;
|
|
if(!_of->seekable){
|
|
if(_of->ready_state<OP_STREAMSET&&_of->ready_state!=OP_PARTOPEN){
|
|
return NULL;
|
|
}
|
|
_li=0;
|
|
}
|
|
else if(_li<0)_li=_of->ready_state>=OP_STREAMSET?_of->cur_link:0;
|
|
return &_of->links[_li].tags;
|
|
}
|
|
|
|
int op_current_link(const OggOpusFile *_of){
|
|
if(OP_UNLIKELY(_of->ready_state<OP_OPENED))return OP_EINVAL;
|
|
return _of->cur_link;
|
|
}
|
|
|
|
/*Compute an average bitrate given a byte and sample count.
|
|
Return: The bitrate in bits per second.*/
|
|
static opus_int32 op_calc_bitrate(opus_int64 _bytes,ogg_int64_t _samples){
|
|
/*These rates are absurd, but let's handle them anyway.*/
|
|
if(OP_UNLIKELY(_bytes>(OP_INT64_MAX-(_samples>>1))/(48000*8))){
|
|
ogg_int64_t den;
|
|
if(OP_UNLIKELY(_bytes/(OP_INT32_MAX/(48000*8))>=_samples)){
|
|
return OP_INT32_MAX;
|
|
}
|
|
den=_samples/(48000*8);
|
|
return (opus_int32)((_bytes+(den>>1))/den);
|
|
}
|
|
if(OP_UNLIKELY(_samples<=0))return OP_INT32_MAX;
|
|
/*This can't actually overflow in normal operation: even with a pre-skip of
|
|
545 2.5 ms frames with 8 streams running at 1282*8+1 bytes per packet
|
|
(1275 byte frames + Opus framing overhead + Ogg lacing values), that all
|
|
produce a single sample of decoded output, we still don't top 45 Mbps.
|
|
The only way to get bitrates larger than that is with excessive Opus
|
|
padding, more encoded streams than output channels, or lots and lots of
|
|
Ogg pages with no packets on them.*/
|
|
return (opus_int32)OP_MIN((_bytes*48000*8+(_samples>>1))/_samples,
|
|
OP_INT32_MAX);
|
|
}
|
|
|
|
opus_int32 op_bitrate(const OggOpusFile *_of,int _li){
|
|
if(OP_UNLIKELY(_of->ready_state<OP_OPENED)||OP_UNLIKELY(!_of->seekable)
|
|
||OP_UNLIKELY(_li>=_of->nlinks)){
|
|
return OP_EINVAL;
|
|
}
|
|
return op_calc_bitrate(op_raw_total(_of,_li),op_pcm_total(_of,_li));
|
|
}
|
|
|
|
opus_int32 op_bitrate_instant(OggOpusFile *_of){
|
|
ogg_int64_t samples_tracked;
|
|
opus_int32 ret;
|
|
if(OP_UNLIKELY(_of->ready_state<OP_OPENED))return OP_EINVAL;
|
|
samples_tracked=_of->samples_tracked;
|
|
if(OP_UNLIKELY(samples_tracked==0))return OP_FALSE;
|
|
ret=op_calc_bitrate(_of->bytes_tracked,samples_tracked);
|
|
_of->bytes_tracked=0;
|
|
_of->samples_tracked=0;
|
|
return ret;
|
|
}
|
|
|
|
/*Fetch and process a page.
|
|
This handles the case where we're at a bitstream boundary and dumps the
|
|
decoding machine.
|
|
If the decoding machine is unloaded, it loads it.
|
|
It also keeps prev_packet_gp up to date (seek and read both use this).
|
|
Return: <0) Error, OP_HOLE (lost packet), or OP_EOF.
|
|
0) Got at least one audio data packet.*/
|
|
static int op_fetch_and_process_page(OggOpusFile *_of,
|
|
ogg_page *_og,opus_int64 _page_offset,int _spanp,int _ignore_holes){
|
|
OggOpusLink *links;
|
|
ogg_uint32_t cur_serialno;
|
|
int seekable;
|
|
int cur_link;
|
|
int ret;
|
|
/*We shouldn't get here if we have unprocessed packets.*/
|
|
OP_ASSERT(_of->ready_state<OP_INITSET||_of->op_pos>=_of->op_count);
|
|
seekable=_of->seekable;
|
|
links=_of->links;
|
|
cur_link=seekable?_of->cur_link:0;
|
|
cur_serialno=links[cur_link].serialno;
|
|
/*Handle one page.*/
|
|
for(;;){
|
|
ogg_page og;
|
|
OP_ASSERT(_of->ready_state>=OP_OPENED);
|
|
/*If we were given a page to use, use it.*/
|
|
if(_og!=NULL){
|
|
*&og=*_og;
|
|
_og=NULL;
|
|
}
|
|
/*Keep reading until we get a page with the correct serialno.*/
|
|
else _page_offset=op_get_next_page(_of,&og,_of->end);
|
|
/*EOF: Leave uninitialized.*/
|
|
if(_page_offset<0)return _page_offset<OP_FALSE?(int)_page_offset:OP_EOF;
|
|
if(OP_LIKELY(_of->ready_state>=OP_STREAMSET)
|
|
&&cur_serialno!=(ogg_uint32_t)ogg_page_serialno(&og)){
|
|
/*Two possibilities:
|
|
1) Another stream is multiplexed into this logical section, or*/
|
|
if(OP_LIKELY(!ogg_page_bos(&og)))continue;
|
|
/* 2) Our decoding just traversed a bitstream boundary.*/
|
|
if(!_spanp)return OP_EOF;
|
|
if(OP_LIKELY(_of->ready_state>=OP_INITSET))op_decode_clear(_of);
|
|
}
|
|
/*Bitrate tracking: add the header's bytes here.
|
|
The body bytes are counted when we consume the packets.*/
|
|
else _of->bytes_tracked+=og.header_len;
|
|
/*Do we need to load a new machine before submitting the page?
|
|
This is different in the seekable and non-seekable cases.
|
|
In the seekable case, we already have all the header information loaded
|
|
and cached.
|
|
We just initialize the machine with it and continue on our merry way.
|
|
In the non-seekable (streaming) case, we'll only be at a boundary if we
|
|
just left the previous logical bitstream, and we're now nominally at the
|
|
header of the next bitstream.*/
|
|
if(OP_UNLIKELY(_of->ready_state<OP_STREAMSET)){
|
|
if(seekable){
|
|
ogg_uint32_t serialno;
|
|
int nlinks;
|
|
int li;
|
|
serialno=ogg_page_serialno(&og);
|
|
/*Match the serialno to bitstream section.
|
|
We use this rather than offset positions to avoid problems near
|
|
logical bitstream boundaries.*/
|
|
nlinks=_of->nlinks;
|
|
for(li=0;li<nlinks&&links[li].serialno!=serialno;li++);
|
|
/*Not a desired Opus bitstream section.
|
|
Keep trying.*/
|
|
if(li>=nlinks)continue;
|
|
cur_serialno=serialno;
|
|
_of->cur_link=cur_link=li;
|
|
ogg_stream_reset_serialno(&_of->os,serialno);
|
|
_of->ready_state=OP_STREAMSET;
|
|
/*If we're at the start of this link, initialize the granule position
|
|
and pre-skip tracking.*/
|
|
if(_page_offset<=links[cur_link].data_offset){
|
|
_of->prev_packet_gp=links[cur_link].pcm_start;
|
|
_of->prev_page_offset=-1;
|
|
_of->cur_discard_count=links[cur_link].head.pre_skip;
|
|
/*Ignore a hole at the start of a new link (this is common for
|
|
streams joined in the middle) or after seeking.*/
|
|
_ignore_holes=1;
|
|
}
|
|
}
|
|
else{
|
|
do{
|
|
/*We're streaming.
|
|
Fetch the two header packets, build the info struct.*/
|
|
ret=op_fetch_headers(_of,&links[0].head,&links[0].tags,
|
|
NULL,NULL,NULL,&og);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
/*op_find_initial_pcm_offset() will suppress any initial hole for us,
|
|
so no need to set _ignore_holes.*/
|
|
ret=op_find_initial_pcm_offset(_of,links,&og);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
_of->links[0].serialno=cur_serialno=_of->os.serialno;
|
|
_of->cur_link++;
|
|
}
|
|
/*If the link was empty, keep going, because we already have the
|
|
BOS page of the next one in og.*/
|
|
while(OP_UNLIKELY(ret>0));
|
|
/*If we didn't get any packets out of op_find_initial_pcm_offset(),
|
|
keep going (this is possible if end-trimming trimmed them all).*/
|
|
if(_of->op_count<=0)continue;
|
|
/*Otherwise, we're done.
|
|
TODO: This resets bytes_tracked, which misses the header bytes
|
|
already processed by op_find_initial_pcm_offset().*/
|
|
ret=op_make_decode_ready(_of);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
return 0;
|
|
}
|
|
}
|
|
/*The buffered page is the data we want, and we're ready for it.
|
|
Add it to the stream state.*/
|
|
if(OP_UNLIKELY(_of->ready_state==OP_STREAMSET)){
|
|
ret=op_make_decode_ready(_of);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
}
|
|
/*Extract all the packets from the current page.*/
|
|
ogg_stream_pagein(&_of->os,&og);
|
|
if(OP_LIKELY(_of->ready_state>=OP_INITSET)){
|
|
opus_int32 total_duration;
|
|
int durations[255];
|
|
int op_count;
|
|
total_duration=op_collect_audio_packets(_of,durations);
|
|
if(OP_UNLIKELY(total_duration<0)){
|
|
/*Drain the packets from the page anyway.*/
|
|
total_duration=op_collect_audio_packets(_of,durations);
|
|
OP_ASSERT(total_duration>=0);
|
|
/*Report holes to the caller.*/
|
|
if(!_ignore_holes)return OP_HOLE;
|
|
}
|
|
op_count=_of->op_count;
|
|
/*If we found at least one audio data packet, compute per-packet granule
|
|
positions for them.*/
|
|
if(op_count>0){
|
|
ogg_int64_t diff;
|
|
ogg_int64_t prev_packet_gp;
|
|
ogg_int64_t cur_packet_gp;
|
|
ogg_int64_t cur_page_gp;
|
|
int cur_page_eos;
|
|
int pi;
|
|
cur_page_gp=_of->op[op_count-1].granulepos;
|
|
cur_page_eos=_of->op[op_count-1].e_o_s;
|
|
prev_packet_gp=_of->prev_packet_gp;
|
|
if(OP_UNLIKELY(prev_packet_gp==-1)){
|
|
opus_int32 cur_discard_count;
|
|
/*This is the first call after a raw seek.
|
|
Try to reconstruct prev_packet_gp from scratch.*/
|
|
OP_ASSERT(seekable);
|
|
if(OP_UNLIKELY(cur_page_eos)){
|
|
/*If the first page we hit after our seek was the EOS page, and
|
|
we didn't start from data_offset or before, we don't have
|
|
enough information to do end-trimming.
|
|
Proceed to the next link, rather than risk playing back some
|
|
samples that shouldn't have been played.*/
|
|
_of->op_count=0;
|
|
continue;
|
|
}
|
|
/*By default discard 80 ms of data after a seek, unless we seek
|
|
into the pre-skip region.*/
|
|
cur_discard_count=80*48;
|
|
cur_page_gp=_of->op[op_count-1].granulepos;
|
|
/*Try to initialize prev_packet_gp.
|
|
If the current page had packets but didn't have a granule
|
|
position, or the granule position it had was too small (both
|
|
illegal), just use the starting granule position for the link.*/
|
|
prev_packet_gp=links[cur_link].pcm_start;
|
|
if(OP_LIKELY(cur_page_gp!=-1)){
|
|
op_granpos_add(&prev_packet_gp,cur_page_gp,-total_duration);
|
|
}
|
|
if(OP_LIKELY(!op_granpos_diff(&diff,
|
|
prev_packet_gp,links[cur_link].pcm_start))){
|
|
opus_int32 pre_skip;
|
|
/*If we start at the beginning of the pre-skip region, or we're
|
|
at least 80 ms from the end of the pre-skip region, we discard
|
|
to the end of the pre-skip region.
|
|
Otherwise, we still use the 80 ms default, which will discard
|
|
past the end of the pre-skip region.*/
|
|
pre_skip=links[cur_link].head.pre_skip;
|
|
if(diff>=0&&diff<=OP_MAX(0,pre_skip-80*48)){
|
|
cur_discard_count=pre_skip-(int)diff;
|
|
}
|
|
}
|
|
_of->cur_discard_count=cur_discard_count;
|
|
}
|
|
if(OP_UNLIKELY(cur_page_gp==-1)){
|
|
/*This page had completed packets but didn't have a valid granule
|
|
position.
|
|
This is illegal, but we'll try to handle it by continuing to count
|
|
forwards from the previous page.*/
|
|
if(op_granpos_add(&cur_page_gp,prev_packet_gp,total_duration)<0){
|
|
/*The timestamp for this page overflowed.*/
|
|
cur_page_gp=links[cur_link].pcm_end;
|
|
}
|
|
}
|
|
/*If we hit the last page, handle end-trimming.*/
|
|
if(OP_UNLIKELY(cur_page_eos)
|
|
&&OP_LIKELY(!op_granpos_diff(&diff,cur_page_gp,prev_packet_gp))
|
|
&&OP_LIKELY(diff<total_duration)){
|
|
cur_packet_gp=prev_packet_gp;
|
|
for(pi=0;pi<op_count;pi++){
|
|
diff=durations[pi]-diff;
|
|
/*If we have samples to trim...*/
|
|
if(diff>0){
|
|
/*If we trimmed the entire packet, stop (the spec says encoders
|
|
shouldn't do this, but we support it anyway).*/
|
|
if(OP_UNLIKELY(diff>durations[pi]))break;
|
|
cur_packet_gp=cur_page_gp;
|
|
/*Move the EOS flag to this packet, if necessary, so we'll trim
|
|
the samples during decode.*/
|
|
_of->op[pi].e_o_s=1;
|
|
}
|
|
else{
|
|
/*Update the granule position as normal.*/
|
|
OP_ALWAYS_TRUE(!op_granpos_add(&cur_packet_gp,
|
|
cur_packet_gp,durations[pi]));
|
|
}
|
|
_of->op[pi].granulepos=cur_packet_gp;
|
|
OP_ALWAYS_TRUE(!op_granpos_diff(&diff,cur_page_gp,cur_packet_gp));
|
|
}
|
|
}
|
|
else{
|
|
/*Propagate timestamps to earlier packets.
|
|
op_granpos_add(&prev_packet_gp,prev_packet_gp,total_duration)
|
|
should succeed and give prev_packet_gp==cur_page_gp.
|
|
But we don't bother to check that, as there isn't much we can do
|
|
if it's not true, and it actually will not be true on the first
|
|
page after a seek, if there was a continued packet.
|
|
The only thing we guarantee is that the start and end granule
|
|
positions of the packets are valid, and that they are monotonic
|
|
within a page.
|
|
They might be completely out of range for this link (we'll check
|
|
that elsewhere), or non-monotonic between pages.*/
|
|
if(OP_UNLIKELY(op_granpos_add(&prev_packet_gp,
|
|
cur_page_gp,-total_duration)<0)){
|
|
/*The starting timestamp for the first packet on this page
|
|
underflowed.
|
|
This is illegal, but we ignore it.*/
|
|
prev_packet_gp=0;
|
|
}
|
|
for(pi=0;pi<op_count;pi++){
|
|
if(OP_UNLIKELY(op_granpos_add(&cur_packet_gp,
|
|
cur_page_gp,-total_duration)<0)){
|
|
/*The start timestamp for this packet underflowed.
|
|
This is illegal, but we ignore it.*/
|
|
cur_packet_gp=0;
|
|
}
|
|
total_duration-=durations[pi];
|
|
OP_ASSERT(total_duration>=0);
|
|
OP_ALWAYS_TRUE(!op_granpos_add(&cur_packet_gp,
|
|
cur_packet_gp,durations[pi]));
|
|
_of->op[pi].granulepos=cur_packet_gp;
|
|
}
|
|
OP_ASSERT(total_duration==0);
|
|
}
|
|
_of->prev_packet_gp=prev_packet_gp;
|
|
_of->prev_page_offset=_page_offset;
|
|
_of->op_count=pi;
|
|
/*If end-trimming didn't trim all the packets, we're done.*/
|
|
if(OP_LIKELY(pi>0))return 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int op_raw_seek(OggOpusFile *_of,opus_int64 _pos){
|
|
int ret;
|
|
if(OP_UNLIKELY(_of->ready_state<OP_OPENED))return OP_EINVAL;
|
|
/*Don't dump the decoder state if we can't seek.*/
|
|
if(OP_UNLIKELY(!_of->seekable))return OP_ENOSEEK;
|
|
if(OP_UNLIKELY(_pos<0)||OP_UNLIKELY(_pos>_of->end))return OP_EINVAL;
|
|
/*Clear out any buffered, decoded data.*/
|
|
op_decode_clear(_of);
|
|
_of->bytes_tracked=0;
|
|
_of->samples_tracked=0;
|
|
ret=op_seek_helper(_of,_pos);
|
|
if(OP_UNLIKELY(ret<0))return OP_EREAD;
|
|
ret=op_fetch_and_process_page(_of,NULL,-1,1,1);
|
|
/*If we hit EOF, op_fetch_and_process_page() leaves us uninitialized.
|
|
Instead, jump to the end.*/
|
|
if(ret==OP_EOF){
|
|
int cur_link;
|
|
op_decode_clear(_of);
|
|
cur_link=_of->nlinks-1;
|
|
_of->cur_link=cur_link;
|
|
_of->prev_packet_gp=_of->links[cur_link].pcm_end;
|
|
_of->cur_discard_count=0;
|
|
ret=0;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/*Convert a PCM offset relative to the start of the whole stream to a granule
|
|
position in an individual link.*/
|
|
static ogg_int64_t op_get_granulepos(const OggOpusFile *_of,
|
|
ogg_int64_t _pcm_offset,int *_li){
|
|
const OggOpusLink *links;
|
|
ogg_int64_t duration;
|
|
int nlinks;
|
|
int li;
|
|
OP_ASSERT(_pcm_offset>=0);
|
|
nlinks=_of->nlinks;
|
|
links=_of->links;
|
|
for(li=0;OP_LIKELY(li<nlinks);li++){
|
|
ogg_int64_t pcm_start;
|
|
opus_int32 pre_skip;
|
|
pcm_start=links[li].pcm_start;
|
|
pre_skip=links[li].head.pre_skip;
|
|
OP_ALWAYS_TRUE(!op_granpos_diff(&duration,links[li].pcm_end,pcm_start));
|
|
duration-=pre_skip;
|
|
if(_pcm_offset<duration){
|
|
_pcm_offset+=pre_skip;
|
|
if(OP_UNLIKELY(pcm_start>OP_INT64_MAX-_pcm_offset)){
|
|
/*Adding this amount to the granule position would overflow the positive
|
|
half of its 64-bit range.
|
|
Since signed overflow is undefined in C, do it in a way the compiler
|
|
isn't allowed to screw up.*/
|
|
_pcm_offset-=OP_INT64_MAX-pcm_start+1;
|
|
pcm_start=OP_INT64_MIN;
|
|
}
|
|
pcm_start+=_pcm_offset;
|
|
*_li=li;
|
|
return pcm_start;
|
|
}
|
|
_pcm_offset-=duration;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/*A small helper to determine if an Ogg page contains data that continues onto
|
|
a subsequent page.*/
|
|
static int op_page_continues(const ogg_page *_og){
|
|
int nlacing;
|
|
OP_ASSERT(_og->header_len>=27);
|
|
nlacing=_og->header[26];
|
|
OP_ASSERT(_og->header_len>=27+nlacing);
|
|
/*This also correctly handles the (unlikely) case of nlacing==0, because
|
|
0!=255.*/
|
|
return _og->header[27+nlacing-1]==255;
|
|
}
|
|
|
|
/*A small helper to buffer the continued packet data from a page.*/
|
|
static void op_buffer_continued_data(OggOpusFile *_of,ogg_page *_og){
|
|
ogg_packet op;
|
|
ogg_stream_pagein(&_of->os,_og);
|
|
/*Drain any packets that did end on this page (and ignore holes).
|
|
We only care about the continued packet data.*/
|
|
while(ogg_stream_packetout(&_of->os,&op));
|
|
}
|
|
|
|
/*This controls how close the target has to be to use the current stream
|
|
position to subdivide the initial range.
|
|
Two minutes seems to be a good default.*/
|
|
#define OP_CUR_TIME_THRESH (120*48*(opus_int32)1000)
|
|
|
|
/*Note: The OP_SMALL_FOOTPRINT #define doesn't (currently) save much code size,
|
|
but it's meant to serve as documentation for portions of the seeking
|
|
algorithm that are purely optional, to aid others learning from/porting this
|
|
code to other contexts.*/
|
|
/*#define OP_SMALL_FOOTPRINT (1)*/
|
|
|
|
/*Search within link _li for the page with the highest granule position
|
|
preceding (or equal to) _target_gp.
|
|
There is a danger here: missing pages or incorrect frame number information
|
|
in the bitstream could make our task impossible.
|
|
Account for that (and report it as an error condition).*/
|
|
static int op_pcm_seek_page(OggOpusFile *_of,
|
|
ogg_int64_t _target_gp,int _li){
|
|
const OggOpusLink *link;
|
|
ogg_page og;
|
|
ogg_int64_t pcm_pre_skip;
|
|
ogg_int64_t pcm_start;
|
|
ogg_int64_t pcm_end;
|
|
ogg_int64_t best_gp;
|
|
ogg_int64_t diff;
|
|
ogg_uint32_t serialno;
|
|
opus_int32 pre_skip;
|
|
opus_int64 begin;
|
|
opus_int64 end;
|
|
opus_int64 boundary;
|
|
opus_int64 best;
|
|
opus_int64 best_start;
|
|
opus_int64 page_offset;
|
|
opus_int64 d0;
|
|
opus_int64 d1;
|
|
opus_int64 d2;
|
|
int force_bisect;
|
|
int buffering;
|
|
int ret;
|
|
_of->bytes_tracked=0;
|
|
_of->samples_tracked=0;
|
|
link=_of->links+_li;
|
|
best_gp=pcm_start=link->pcm_start;
|
|
pcm_end=link->pcm_end;
|
|
serialno=link->serialno;
|
|
best=best_start=begin=link->data_offset;
|
|
page_offset=-1;
|
|
buffering=0;
|
|
/*We discard the first 80 ms of data after a seek, so seek back that much
|
|
farther.
|
|
If we can't, simply seek to the beginning of the link.*/
|
|
if(OP_UNLIKELY(op_granpos_add(&_target_gp,_target_gp,-80*48)<0)
|
|
||OP_UNLIKELY(op_granpos_cmp(_target_gp,pcm_start)<0)){
|
|
_target_gp=pcm_start;
|
|
}
|
|
/*Special case seeking to the start of the link.*/
|
|
pre_skip=link->head.pre_skip;
|
|
OP_ALWAYS_TRUE(!op_granpos_add(&pcm_pre_skip,pcm_start,pre_skip));
|
|
if(op_granpos_cmp(_target_gp,pcm_pre_skip)<0)end=boundary=begin;
|
|
else{
|
|
end=boundary=link->end_offset;
|
|
#if !defined(OP_SMALL_FOOTPRINT)
|
|
/*If we were decoding from this link, we can narrow the range a bit.*/
|
|
if(_li==_of->cur_link&&_of->ready_state>=OP_INITSET){
|
|
opus_int64 offset;
|
|
int op_count;
|
|
op_count=_of->op_count;
|
|
/*The only way the offset can be invalid _and_ we can fail the granule
|
|
position checks below is if someone changed the contents of the last
|
|
page since we read it.
|
|
We'd be within our rights to just return OP_EBADLINK in that case, but
|
|
we'll simply ignore the current position instead.*/
|
|
offset=_of->offset;
|
|
if(op_count>0&&OP_LIKELY(offset<=end)){
|
|
ogg_int64_t gp;
|
|
/*Make sure the timestamp is valid.
|
|
The granule position might be -1 if we collected the packets from a
|
|
page without a granule position after reporting a hole.*/
|
|
gp=_of->op[op_count-1].granulepos;
|
|
if(OP_LIKELY(gp!=-1)&&OP_LIKELY(op_granpos_cmp(pcm_start,gp)<0)
|
|
&&OP_LIKELY(op_granpos_cmp(pcm_end,gp)>0)){
|
|
OP_ALWAYS_TRUE(!op_granpos_diff(&diff,gp,_target_gp));
|
|
/*We only actually use the current time if either
|
|
a) We can cut off at least half the range, or
|
|
b) We're seeking sufficiently close to the current position that
|
|
it's likely to be informative.
|
|
Otherwise it appears using the whole link range to estimate the
|
|
first seek location gives better results, on average.*/
|
|
if(diff<0){
|
|
OP_ASSERT(offset>=begin);
|
|
if(offset-begin>=end-begin>>1||diff>-OP_CUR_TIME_THRESH){
|
|
best=begin=offset;
|
|
best_gp=pcm_start=gp;
|
|
/*If we have buffered data from a continued packet, remember the
|
|
offset of the previous page's start, so that if we do wind up
|
|
having to seek back here later, we can prime the stream with
|
|
the continued packet data.
|
|
With no continued packet, we remember the end of the page.*/
|
|
best_start=_of->os.body_returned<_of->os.body_fill?
|
|
_of->prev_page_offset:best;
|
|
/*If there's completed packets and data in the stream state,
|
|
prev_page_offset should always be set.*/
|
|
OP_ASSERT(best_start>=0);
|
|
/*Buffer any continued packet data starting from here.*/
|
|
buffering=1;
|
|
}
|
|
}
|
|
else{
|
|
ogg_int64_t prev_page_gp;
|
|
/*We might get lucky and already have the packet with the target
|
|
buffered.
|
|
Worth checking.
|
|
For very small files (with all of the data in a single page,
|
|
generally 1 second or less), we can loop them continuously
|
|
without seeking at all.*/
|
|
OP_ALWAYS_TRUE(!op_granpos_add(&prev_page_gp,_of->op[0].granulepos,
|
|
-op_get_packet_duration(_of->op[0].packet,_of->op[0].bytes)));
|
|
if(op_granpos_cmp(prev_page_gp,_target_gp)<=0){
|
|
/*Don't call op_decode_clear(), because it will dump our
|
|
packets.*/
|
|
_of->op_pos=0;
|
|
_of->od_buffer_size=0;
|
|
_of->prev_packet_gp=prev_page_gp;
|
|
/*_of->prev_page_offset already points to the right place.*/
|
|
_of->ready_state=OP_STREAMSET;
|
|
return op_make_decode_ready(_of);
|
|
}
|
|
/*No such luck.
|
|
Check if we can cut off at least half the range, though.*/
|
|
if(offset-begin<=end-begin>>1||diff<OP_CUR_TIME_THRESH){
|
|
/*We really want the page start here, but this will do.*/
|
|
end=boundary=offset;
|
|
pcm_end=gp;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
/*This code was originally based on the "new search algorithm by HB (Nicholas
|
|
Vinen)" from libvorbisfile.
|
|
It has been modified substantially since.*/
|
|
op_decode_clear(_of);
|
|
if(!buffering)ogg_stream_reset_serialno(&_of->os,serialno);
|
|
_of->cur_link=_li;
|
|
_of->ready_state=OP_STREAMSET;
|
|
/*Initialize the interval size history.*/
|
|
d2=d1=d0=end-begin;
|
|
force_bisect=0;
|
|
while(begin<end){
|
|
opus_int64 bisect;
|
|
opus_int64 next_boundary;
|
|
opus_int32 chunk_size;
|
|
if(end-begin<OP_CHUNK_SIZE)bisect=begin;
|
|
else{
|
|
/*Update the interval size history.*/
|
|
d0=d1>>1;
|
|
d1=d2>>1;
|
|
d2=end-begin>>1;
|
|
if(force_bisect)bisect=begin+(end-begin>>1);
|
|
else{
|
|
ogg_int64_t diff2;
|
|
OP_ALWAYS_TRUE(!op_granpos_diff(&diff,_target_gp,pcm_start));
|
|
OP_ALWAYS_TRUE(!op_granpos_diff(&diff2,pcm_end,pcm_start));
|
|
/*Take a (pretty decent) guess.*/
|
|
bisect=begin+op_rescale64(diff,diff2,end-begin)-OP_CHUNK_SIZE;
|
|
}
|
|
if(bisect-OP_CHUNK_SIZE<begin)bisect=begin;
|
|
force_bisect=0;
|
|
}
|
|
if(bisect!=_of->offset){
|
|
/*Discard any buffered continued packet data.*/
|
|
if(buffering)ogg_stream_reset(&_of->os);
|
|
buffering=0;
|
|
page_offset=-1;
|
|
ret=op_seek_helper(_of,bisect);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
}
|
|
chunk_size=OP_CHUNK_SIZE;
|
|
next_boundary=boundary;
|
|
/*Now scan forward and figure out where we landed.
|
|
In the ideal case, we will see a page with a granule position at or
|
|
before our target, followed by a page with a granule position after our
|
|
target (or the end of the search interval).
|
|
Then we can just drop out and will have all of the data we need with no
|
|
additional seeking.
|
|
If we landed too far before, or after, we'll break out and do another
|
|
bisection.*/
|
|
while(begin<end){
|
|
page_offset=op_get_next_page(_of,&og,boundary);
|
|
if(page_offset<0){
|
|
if(page_offset<OP_FALSE)return (int)page_offset;
|
|
/*There are no more pages in our interval from our stream with a valid
|
|
timestamp that start at position bisect or later.*/
|
|
/*If we scanned the whole interval, we're done.*/
|
|
if(bisect<=begin+1)end=begin;
|
|
else{
|
|
/*Otherwise, back up one chunk.
|
|
First, discard any data from a continued packet.*/
|
|
if(buffering)ogg_stream_reset(&_of->os);
|
|
buffering=0;
|
|
bisect=OP_MAX(bisect-chunk_size,begin);
|
|
ret=op_seek_helper(_of,bisect);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
/*Bump up the chunk size.*/
|
|
chunk_size=OP_MIN(2*chunk_size,OP_CHUNK_SIZE_MAX);
|
|
/*If we did find a page from another stream or without a timestamp,
|
|
don't read past it.*/
|
|
boundary=next_boundary;
|
|
}
|
|
}
|
|
else{
|
|
ogg_int64_t gp;
|
|
int has_packets;
|
|
/*Save the offset of the first page we found after the seek, regardless
|
|
of the stream it came from or whether or not it has a timestamp.*/
|
|
next_boundary=OP_MIN(page_offset,next_boundary);
|
|
if(serialno!=(ogg_uint32_t)ogg_page_serialno(&og))continue;
|
|
has_packets=ogg_page_packets(&og)>0;
|
|
/*Force the gp to -1 (as it should be per spec) if no packets end on
|
|
this page.
|
|
Otherwise we might get confused when we try to pull out a packet
|
|
with that timestamp and can't find it.*/
|
|
gp=has_packets?ogg_page_granulepos(&og):-1;
|
|
if(gp==-1){
|
|
if(buffering){
|
|
if(OP_LIKELY(!has_packets))ogg_stream_pagein(&_of->os,&og);
|
|
else{
|
|
/*If packets did end on this page, but we still didn't have a
|
|
valid granule position (in violation of the spec!), stop
|
|
buffering continued packet data.
|
|
Otherwise we might continue past the packet we actually
|
|
wanted.*/
|
|
ogg_stream_reset(&_of->os);
|
|
buffering=0;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if(op_granpos_cmp(gp,_target_gp)<0){
|
|
/*We found a page that ends before our target.
|
|
Advance to the raw offset of the next page.*/
|
|
begin=_of->offset;
|
|
if(OP_UNLIKELY(op_granpos_cmp(pcm_start,gp)>0)
|
|
||OP_UNLIKELY(op_granpos_cmp(pcm_end,gp)<0)){
|
|
/*Don't let pcm_start get out of range!
|
|
That could happen with an invalid timestamp.*/
|
|
break;
|
|
}
|
|
/*Save the byte offset of the end of the page with this granule
|
|
position.*/
|
|
best=best_start=begin;
|
|
/*Buffer any data from a continued packet, if necessary.
|
|
This avoids the need to seek back here if the next timestamp we
|
|
encounter while scanning forward lies after our target.*/
|
|
if(buffering)ogg_stream_reset(&_of->os);
|
|
if(op_page_continues(&og)){
|
|
op_buffer_continued_data(_of,&og);
|
|
/*If we have a continued packet, remember the offset of this
|
|
page's start, so that if we do wind up having to seek back here
|
|
later, we can prime the stream with the continued packet data.
|
|
With no continued packet, we remember the end of the page.*/
|
|
best_start=page_offset;
|
|
}
|
|
/*Then force buffering on, so that if a packet starts (but does not
|
|
end) on the next page, we still avoid the extra seek back.*/
|
|
buffering=1;
|
|
best_gp=pcm_start=gp;
|
|
OP_ALWAYS_TRUE(!op_granpos_diff(&diff,_target_gp,pcm_start));
|
|
/*If we're more than a second away from our target, break out and
|
|
do another bisection.*/
|
|
if(diff>48000)break;
|
|
/*Otherwise, keep scanning forward (do NOT use begin+1).*/
|
|
bisect=begin;
|
|
}
|
|
else{
|
|
/*We found a page that ends after our target.*/
|
|
/*If we scanned the whole interval before we found it, we're done.*/
|
|
if(bisect<=begin+1)end=begin;
|
|
else{
|
|
end=bisect;
|
|
/*In later iterations, don't read past the first page we found.*/
|
|
boundary=next_boundary;
|
|
/*If we're not making much progress shrinking the interval size,
|
|
start forcing straight bisection to limit the worst case.*/
|
|
force_bisect=end-begin>d0*2;
|
|
/*Don't let pcm_end get out of range!
|
|
That could happen with an invalid timestamp.*/
|
|
if(OP_LIKELY(op_granpos_cmp(pcm_end,gp)>0)
|
|
&&OP_LIKELY(op_granpos_cmp(pcm_start,gp)<=0)){
|
|
pcm_end=gp;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/*Found our page.*/
|
|
OP_ASSERT(op_granpos_cmp(best_gp,pcm_start)>=0);
|
|
/*Seek, if necessary.
|
|
If we were buffering data from a continued packet, we should be able to
|
|
continue to scan forward to get the rest of the data (even if
|
|
page_offset==-1).
|
|
Otherwise, we need to seek back to best_start.*/
|
|
if(!buffering){
|
|
if(best_start!=page_offset){
|
|
page_offset=-1;
|
|
ret=op_seek_helper(_of,best_start);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
}
|
|
if(best_start<best){
|
|
/*Retrieve the page at best_start, if we do not already have it.*/
|
|
if(page_offset<0){
|
|
page_offset=op_get_next_page(_of,&og,link->end_offset);
|
|
if(OP_UNLIKELY(page_offset<OP_FALSE))return (int)page_offset;
|
|
if(OP_UNLIKELY(page_offset!=best_start))return OP_EBADLINK;
|
|
}
|
|
op_buffer_continued_data(_of,&og);
|
|
page_offset=-1;
|
|
}
|
|
}
|
|
/*Update prev_packet_gp to allow per-packet granule position assignment.*/
|
|
_of->prev_packet_gp=best_gp;
|
|
_of->prev_page_offset=best_start;
|
|
ret=op_fetch_and_process_page(_of,page_offset<0?NULL:&og,page_offset,0,1);
|
|
if(OP_UNLIKELY(ret<0))return OP_EBADLINK;
|
|
/*Verify result.*/
|
|
if(OP_UNLIKELY(op_granpos_cmp(_of->prev_packet_gp,_target_gp)>0)){
|
|
return OP_EBADLINK;
|
|
}
|
|
/*Our caller will set cur_discard_count to handle pre-roll.*/
|
|
return 0;
|
|
}
|
|
|
|
int op_pcm_seek(OggOpusFile *_of,ogg_int64_t _pcm_offset){
|
|
const OggOpusLink *link;
|
|
ogg_int64_t pcm_start;
|
|
ogg_int64_t target_gp;
|
|
ogg_int64_t prev_packet_gp;
|
|
ogg_int64_t skip;
|
|
ogg_int64_t diff;
|
|
int op_count;
|
|
int op_pos;
|
|
int ret;
|
|
int li;
|
|
if(OP_UNLIKELY(_of->ready_state<OP_OPENED))return OP_EINVAL;
|
|
if(OP_UNLIKELY(!_of->seekable))return OP_ENOSEEK;
|
|
if(OP_UNLIKELY(_pcm_offset<0))return OP_EINVAL;
|
|
target_gp=op_get_granulepos(_of,_pcm_offset,&li);
|
|
if(OP_UNLIKELY(target_gp==-1))return OP_EINVAL;
|
|
link=_of->links+li;
|
|
pcm_start=link->pcm_start;
|
|
OP_ALWAYS_TRUE(!op_granpos_diff(&_pcm_offset,target_gp,pcm_start));
|
|
#if !defined(OP_SMALL_FOOTPRINT)
|
|
/*For small (90 ms or less) forward seeks within the same link, just decode
|
|
forward.
|
|
This also optimizes the case of seeking to the current position.*/
|
|
if(li==_of->cur_link&&_of->ready_state>=OP_INITSET){
|
|
ogg_int64_t gp;
|
|
gp=_of->prev_packet_gp;
|
|
if(OP_LIKELY(gp!=-1)){
|
|
int nbuffered;
|
|
nbuffered=OP_MAX(_of->od_buffer_size-_of->od_buffer_pos,0);
|
|
OP_ALWAYS_TRUE(!op_granpos_add(&gp,gp,-nbuffered));
|
|
/*We do _not_ add cur_discard_count to gp.
|
|
Otherwise the total amount to discard could grow without bound, and it
|
|
would be better just to do a full seek.*/
|
|
if(OP_LIKELY(!op_granpos_diff(&diff,gp,pcm_start))){
|
|
ogg_int64_t discard_count;
|
|
discard_count=_pcm_offset-diff;
|
|
/*We use a threshold of 90 ms instead of 80, since 80 ms is the
|
|
_minimum_ we would have discarded after a full seek.
|
|
Assuming 20 ms frames (the default), we'd discard 90 ms on average.*/
|
|
if(discard_count>=0&&OP_UNLIKELY(discard_count<90*48)){
|
|
_of->cur_discard_count=(opus_int32)discard_count;
|
|
return 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
ret=op_pcm_seek_page(_of,target_gp,li);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
/*Now skip samples until we actually get to our target.*/
|
|
/*Figure out where we should skip to.*/
|
|
if(_pcm_offset<=link->head.pre_skip)skip=0;
|
|
else skip=OP_MAX(_pcm_offset-80*48,0);
|
|
OP_ASSERT(_pcm_offset-skip>=0);
|
|
OP_ASSERT(_pcm_offset-skip<OP_INT32_MAX-120*48);
|
|
/*Skip packets until we find one with samples past our skip target.*/
|
|
for(;;){
|
|
op_count=_of->op_count;
|
|
prev_packet_gp=_of->prev_packet_gp;
|
|
for(op_pos=_of->op_pos;op_pos<op_count;op_pos++){
|
|
ogg_int64_t cur_packet_gp;
|
|
cur_packet_gp=_of->op[op_pos].granulepos;
|
|
if(OP_LIKELY(!op_granpos_diff(&diff,cur_packet_gp,pcm_start))
|
|
&&diff>skip){
|
|
break;
|
|
}
|
|
prev_packet_gp=cur_packet_gp;
|
|
}
|
|
_of->prev_packet_gp=prev_packet_gp;
|
|
_of->op_pos=op_pos;
|
|
if(op_pos<op_count)break;
|
|
/*We skipped all the packets on this page.
|
|
Fetch another.*/
|
|
ret=op_fetch_and_process_page(_of,NULL,-1,0,1);
|
|
if(OP_UNLIKELY(ret<0))return OP_EBADLINK;
|
|
}
|
|
OP_ALWAYS_TRUE(!op_granpos_diff(&diff,prev_packet_gp,pcm_start));
|
|
/*We skipped too far.
|
|
Either the timestamps were illegal or there was a hole in the data.*/
|
|
if(diff>skip)return OP_EBADLINK;
|
|
OP_ASSERT(_pcm_offset-diff<OP_INT32_MAX);
|
|
/*TODO: If there are further holes/illegal timestamps, we still won't decode
|
|
to the correct sample.
|
|
However, at least op_pcm_tell() will report the correct value immediately
|
|
after returning.*/
|
|
_of->cur_discard_count=(opus_int32)(_pcm_offset-diff);
|
|
return 0;
|
|
}
|
|
|
|
opus_int64 op_raw_tell(const OggOpusFile *_of){
|
|
if(OP_UNLIKELY(_of->ready_state<OP_OPENED))return OP_EINVAL;
|
|
return _of->offset;
|
|
}
|
|
|
|
/*Convert a granule position from a given link to a PCM offset relative to the
|
|
start of the whole stream.
|
|
For unseekable sources, this gets reset to 0 at the beginning of each link.*/
|
|
static ogg_int64_t op_get_pcm_offset(const OggOpusFile *_of,
|
|
ogg_int64_t _gp,int _li){
|
|
const OggOpusLink *links;
|
|
ogg_int64_t pcm_offset;
|
|
ogg_int64_t delta;
|
|
int li;
|
|
links=_of->links;
|
|
pcm_offset=0;
|
|
OP_ASSERT(_li<_of->nlinks);
|
|
for(li=0;li<_li;li++){
|
|
OP_ALWAYS_TRUE(!op_granpos_diff(&delta,
|
|
links[li].pcm_end,links[li].pcm_start));
|
|
delta-=links[li].head.pre_skip;
|
|
pcm_offset+=delta;
|
|
}
|
|
OP_ASSERT(_li>=0);
|
|
if(_of->seekable&&OP_UNLIKELY(op_granpos_cmp(_gp,links[_li].pcm_end)>0)){
|
|
_gp=links[_li].pcm_end;
|
|
}
|
|
if(OP_LIKELY(op_granpos_cmp(_gp,links[_li].pcm_start)>0)){
|
|
if(OP_UNLIKELY(op_granpos_diff(&delta,_gp,links[_li].pcm_start)<0)){
|
|
/*This means an unseekable stream claimed to have a page from more than
|
|
2 billion days after we joined.*/
|
|
OP_ASSERT(!_of->seekable);
|
|
return OP_INT64_MAX;
|
|
}
|
|
if(delta<links[_li].head.pre_skip)delta=0;
|
|
else delta-=links[_li].head.pre_skip;
|
|
/*In the seekable case, _gp was limited by pcm_end.
|
|
In the unseekable case, pcm_offset should be 0.*/
|
|
OP_ASSERT(pcm_offset<=OP_INT64_MAX-delta);
|
|
pcm_offset+=delta;
|
|
}
|
|
return pcm_offset;
|
|
}
|
|
|
|
ogg_int64_t op_pcm_tell(const OggOpusFile *_of){
|
|
ogg_int64_t gp;
|
|
int nbuffered;
|
|
int li;
|
|
if(OP_UNLIKELY(_of->ready_state<OP_OPENED))return OP_EINVAL;
|
|
gp=_of->prev_packet_gp;
|
|
if(gp==-1)return 0;
|
|
nbuffered=OP_MAX(_of->od_buffer_size-_of->od_buffer_pos,0);
|
|
OP_ALWAYS_TRUE(!op_granpos_add(&gp,gp,-nbuffered));
|
|
li=_of->seekable?_of->cur_link:0;
|
|
if(op_granpos_add(&gp,gp,_of->cur_discard_count)<0){
|
|
gp=_of->links[li].pcm_end;
|
|
}
|
|
return op_get_pcm_offset(_of,gp,li);
|
|
}
|
|
|
|
void op_set_decode_callback(OggOpusFile *_of,
|
|
op_decode_cb_func _decode_cb,void *_ctx){
|
|
_of->decode_cb=_decode_cb;
|
|
_of->decode_cb_ctx=_ctx;
|
|
}
|
|
|
|
int op_set_gain_offset(OggOpusFile *_of,
|
|
int _gain_type,opus_int32 _gain_offset_q8){
|
|
if(_gain_type!=OP_HEADER_GAIN&&_gain_type!=OP_ALBUM_GAIN
|
|
&&_gain_type!=OP_TRACK_GAIN&&_gain_type!=OP_ABSOLUTE_GAIN){
|
|
return OP_EINVAL;
|
|
}
|
|
_of->gain_type=_gain_type;
|
|
/*The sum of header gain and track gain lies in the range [-65536,65534].
|
|
These bounds allow the offset to set the final value to anywhere in the
|
|
range [-32768,32767], which is what we'll clamp it to before applying.*/
|
|
_of->gain_offset_q8=OP_CLAMP(-98302,_gain_offset_q8,98303);
|
|
op_update_gain(_of);
|
|
return 0;
|
|
}
|
|
|
|
void op_set_dither_enabled(OggOpusFile *_of,int _enabled){
|
|
#if !defined(OP_FIXED_POINT)
|
|
_of->dither_disabled=!_enabled;
|
|
if(!_enabled)_of->dither_mute=65;
|
|
#endif
|
|
}
|
|
|
|
/*Allocate the decoder scratch buffer.
|
|
This is done lazily, since if the user provides large enough buffers, we'll
|
|
never need it.*/
|
|
static int op_init_buffer(OggOpusFile *_of){
|
|
int nchannels_max;
|
|
if(_of->seekable){
|
|
const OggOpusLink *links;
|
|
int nlinks;
|
|
int li;
|
|
links=_of->links;
|
|
nlinks=_of->nlinks;
|
|
nchannels_max=1;
|
|
for(li=0;li<nlinks;li++){
|
|
nchannels_max=OP_MAX(nchannels_max,links[li].head.channel_count);
|
|
}
|
|
}
|
|
else nchannels_max=OP_NCHANNELS_MAX;
|
|
_of->od_buffer=(op_sample *)_ogg_malloc(
|
|
sizeof(*_of->od_buffer)*nchannels_max*120*48);
|
|
if(_of->od_buffer==NULL)return OP_EFAULT;
|
|
return 0;
|
|
}
|
|
|
|
/*Decode a single packet into the target buffer.*/
|
|
static int op_decode(OggOpusFile *_of,op_sample *_pcm,
|
|
const ogg_packet *_op,int _nsamples,int _nchannels){
|
|
int ret;
|
|
/*First we try using the application-provided decode callback.*/
|
|
if(_of->decode_cb!=NULL){
|
|
#if defined(OP_FIXED_POINT)
|
|
ret=(*_of->decode_cb)(_of->decode_cb_ctx,_of->od,_pcm,_op,
|
|
_nsamples,_nchannels,OP_DEC_FORMAT_SHORT,_of->cur_link);
|
|
#else
|
|
ret=(*_of->decode_cb)(_of->decode_cb_ctx,_of->od,_pcm,_op,
|
|
_nsamples,_nchannels,OP_DEC_FORMAT_FLOAT,_of->cur_link);
|
|
#endif
|
|
}
|
|
else ret=OP_DEC_USE_DEFAULT;
|
|
/*If the application didn't want to handle decoding, do it ourselves.*/
|
|
if(ret==OP_DEC_USE_DEFAULT){
|
|
#if defined(OP_FIXED_POINT)
|
|
ret=opus_multistream_decode(_of->od,
|
|
_op->packet,_op->bytes,_pcm,_nsamples,0);
|
|
#else
|
|
ret=opus_multistream_decode_float(_of->od,
|
|
_op->packet,_op->bytes,_pcm,_nsamples,0);
|
|
#endif
|
|
OP_ASSERT(ret<0||ret==_nsamples);
|
|
}
|
|
/*If the application returned a positive value other than 0 or
|
|
OP_DEC_USE_DEFAULT, fail.*/
|
|
else if(OP_UNLIKELY(ret>0))return OP_EBADPACKET;
|
|
if(OP_UNLIKELY(ret<0))return OP_EBADPACKET;
|
|
return ret;
|
|
}
|
|
|
|
/*Read more samples from the stream, using the same API as op_read() or
|
|
op_read_float().*/
|
|
static int op_read_native(OggOpusFile *_of,
|
|
op_sample *_pcm,int _buf_size,int *_li){
|
|
if(OP_UNLIKELY(_of->ready_state<OP_OPENED))return OP_EINVAL;
|
|
for(;;){
|
|
int ret;
|
|
if(OP_LIKELY(_of->ready_state>=OP_INITSET)){
|
|
int nchannels;
|
|
int od_buffer_pos;
|
|
int nsamples;
|
|
int op_pos;
|
|
nchannels=_of->links[_of->seekable?_of->cur_link:0].head.channel_count;
|
|
od_buffer_pos=_of->od_buffer_pos;
|
|
nsamples=_of->od_buffer_size-od_buffer_pos;
|
|
/*If we have buffered samples, return them.*/
|
|
if(nsamples>0){
|
|
if(nsamples*nchannels>_buf_size)nsamples=_buf_size/nchannels;
|
|
memcpy(_pcm,_of->od_buffer+nchannels*od_buffer_pos,
|
|
sizeof(*_pcm)*nchannels*nsamples);
|
|
od_buffer_pos+=nsamples;
|
|
_of->od_buffer_pos=od_buffer_pos;
|
|
if(_li!=NULL)*_li=_of->cur_link;
|
|
return nsamples;
|
|
}
|
|
/*If we have buffered packets, decode one.*/
|
|
op_pos=_of->op_pos;
|
|
if(OP_LIKELY(op_pos<_of->op_count)){
|
|
const ogg_packet *pop;
|
|
ogg_int64_t diff;
|
|
opus_int32 cur_discard_count;
|
|
int duration;
|
|
int trimmed_duration;
|
|
pop=_of->op+op_pos++;
|
|
_of->op_pos=op_pos;
|
|
cur_discard_count=_of->cur_discard_count;
|
|
duration=op_get_packet_duration(pop->packet,pop->bytes);
|
|
/*We don't buffer packets with an invalid TOC sequence.*/
|
|
OP_ASSERT(duration>0);
|
|
trimmed_duration=duration;
|
|
/*Perform end-trimming.*/
|
|
if(OP_UNLIKELY(pop->e_o_s)){
|
|
if(OP_UNLIKELY(op_granpos_cmp(pop->granulepos,
|
|
_of->prev_packet_gp)<=0)){
|
|
trimmed_duration=0;
|
|
}
|
|
else if(OP_LIKELY(!op_granpos_diff(&diff,
|
|
pop->granulepos,_of->prev_packet_gp))){
|
|
trimmed_duration=(int)OP_MIN(diff,trimmed_duration);
|
|
}
|
|
}
|
|
_of->prev_packet_gp=pop->granulepos;
|
|
if(OP_UNLIKELY(duration*nchannels>_buf_size)){
|
|
op_sample *buf;
|
|
/*If the user's buffer is too small, decode into a scratch buffer.*/
|
|
buf=_of->od_buffer;
|
|
if(OP_UNLIKELY(buf==NULL)){
|
|
ret=op_init_buffer(_of);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
buf=_of->od_buffer;
|
|
}
|
|
ret=op_decode(_of,buf,pop,duration,nchannels);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
/*Perform pre-skip/pre-roll.*/
|
|
od_buffer_pos=(int)OP_MIN(trimmed_duration,cur_discard_count);
|
|
cur_discard_count-=od_buffer_pos;
|
|
_of->cur_discard_count=cur_discard_count;
|
|
_of->od_buffer_pos=od_buffer_pos;
|
|
_of->od_buffer_size=trimmed_duration;
|
|
/*Update bitrate tracking based on the actual samples we used from
|
|
what was decoded.*/
|
|
_of->bytes_tracked+=pop->bytes;
|
|
_of->samples_tracked+=trimmed_duration-od_buffer_pos;
|
|
}
|
|
else{
|
|
/*Otherwise decode directly into the user's buffer.*/
|
|
ret=op_decode(_of,_pcm,pop,duration,nchannels);
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
if(OP_LIKELY(trimmed_duration>0)){
|
|
/*Perform pre-skip/pre-roll.*/
|
|
od_buffer_pos=(int)OP_MIN(trimmed_duration,cur_discard_count);
|
|
cur_discard_count-=od_buffer_pos;
|
|
_of->cur_discard_count=cur_discard_count;
|
|
trimmed_duration-=od_buffer_pos;
|
|
if(OP_LIKELY(trimmed_duration>0)
|
|
&&OP_UNLIKELY(od_buffer_pos>0)){
|
|
memmove(_pcm,_pcm+od_buffer_pos*nchannels,
|
|
sizeof(*_pcm)*trimmed_duration*nchannels);
|
|
}
|
|
/*Update bitrate tracking based on the actual samples we used from
|
|
what was decoded.*/
|
|
_of->bytes_tracked+=pop->bytes;
|
|
_of->samples_tracked+=trimmed_duration;
|
|
if(OP_LIKELY(trimmed_duration>0)){
|
|
if(_li!=NULL)*_li=_of->cur_link;
|
|
return trimmed_duration;
|
|
}
|
|
}
|
|
}
|
|
/*Don't grab another page yet.
|
|
This one might have more packets, or might have buffered data now.*/
|
|
continue;
|
|
}
|
|
}
|
|
/*Suck in another page.*/
|
|
ret=op_fetch_and_process_page(_of,NULL,-1,1,0);
|
|
if(OP_UNLIKELY(ret==OP_EOF)){
|
|
if(_li!=NULL)*_li=_of->cur_link;
|
|
return 0;
|
|
}
|
|
if(OP_UNLIKELY(ret<0))return ret;
|
|
}
|
|
}
|
|
|
|
/*A generic filter to apply to the decoded audio data.
|
|
_src is non-const because we will destructively modify the contents of the
|
|
source buffer that we consume in some cases.*/
|
|
typedef int (*op_read_filter_func)(OggOpusFile *_of,void *_dst,int _dst_sz,
|
|
op_sample *_src,int _nsamples,int _nchannels);
|
|
|
|
/*Decode some samples and then apply a custom filter to them.
|
|
This is used to convert to different output formats.*/
|
|
static int op_filter_read_native(OggOpusFile *_of,void *_dst,int _dst_sz,
|
|
op_read_filter_func _filter,int *_li){
|
|
int ret;
|
|
/*Ensure we have some decoded samples in our buffer.*/
|
|
ret=op_read_native(_of,NULL,0,_li);
|
|
/*Now apply the filter to them.*/
|
|
if(OP_LIKELY(ret>=0)&&OP_LIKELY(_of->ready_state>=OP_INITSET)){
|
|
int od_buffer_pos;
|
|
od_buffer_pos=_of->od_buffer_pos;
|
|
ret=_of->od_buffer_size-od_buffer_pos;
|
|
if(OP_LIKELY(ret>0)){
|
|
int nchannels;
|
|
nchannels=_of->links[_of->seekable?_of->cur_link:0].head.channel_count;
|
|
ret=(*_filter)(_of,_dst,_dst_sz,
|
|
_of->od_buffer+nchannels*od_buffer_pos,ret,nchannels);
|
|
OP_ASSERT(ret>=0);
|
|
OP_ASSERT(ret<=_of->od_buffer_size-od_buffer_pos);
|
|
od_buffer_pos+=ret;
|
|
_of->od_buffer_pos=od_buffer_pos;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
#if !defined(OP_FIXED_POINT)||!defined(OP_DISABLE_FLOAT_API)
|
|
|
|
/*Matrices for downmixing from the supported channel counts to stereo.
|
|
The matrices with 5 or more channels are normalized to a total volume of 2.0,
|
|
since most mixes sound too quiet if normalized to 1.0 (as there is generally
|
|
little volume in the side/rear channels).*/
|
|
static const float OP_STEREO_DOWNMIX[OP_NCHANNELS_MAX-2][OP_NCHANNELS_MAX][2]={
|
|
/*3.0*/
|
|
{
|
|
{0.5858F,0.0F},{0.4142F,0.4142F},{0.0F,0.5858F}
|
|
},
|
|
/*quadrophonic*/
|
|
{
|
|
{0.4226F,0.0F},{0.0F,0.4226F},{0.366F,0.2114F},{0.2114F,0.336F}
|
|
},
|
|
/*5.0*/
|
|
{
|
|
{0.651F,0.0F},{0.46F,0.46F},{0.0F,0.651F},{0.5636F,0.3254F},
|
|
{0.3254F,0.5636F}
|
|
},
|
|
/*5.1*/
|
|
{
|
|
{0.529F,0.0F},{0.3741F,0.3741F},{0.0F,0.529F},{0.4582F,0.2645F},
|
|
{0.2645F,0.4582F},{0.3741F,0.3741F}
|
|
},
|
|
/*6.1*/
|
|
{
|
|
{0.4553F,0.0F},{0.322F,0.322F},{0.0F,0.4553F},{0.3943F,0.2277F},
|
|
{0.2277F,0.3943F},{0.2788F,0.2788F},{0.322F,0.322F}
|
|
},
|
|
/*7.1*/
|
|
{
|
|
{0.3886F,0.0F},{0.2748F,0.2748F},{0.0F,0.3886F},{0.3366F,0.1943F},
|
|
{0.1943F,0.3366F},{0.3366F,0.1943F},{0.1943F,0.3366F},{0.2748F,0.2748F}
|
|
}
|
|
};
|
|
|
|
#endif
|
|
|
|
#if defined(OP_FIXED_POINT)
|
|
|
|
/*Matrices for downmixing from the supported channel counts to stereo.
|
|
The matrices with 5 or more channels are normalized to a total volume of 2.0,
|
|
since most mixes sound too quiet if normalized to 1.0 (as there is generally
|
|
little volume in the side/rear channels).
|
|
Hence we keep the coefficients in Q14, so the downmix values won't overflow a
|
|
32-bit number.*/
|
|
static const opus_int16 OP_STEREO_DOWNMIX_Q14
|
|
[OP_NCHANNELS_MAX-2][OP_NCHANNELS_MAX][2]={
|
|
/*3.0*/
|
|
{
|
|
{9598,0},{6786,6786},{0,9598}
|
|
},
|
|
/*quadrophonic*/
|
|
{
|
|
{6924,0},{0,6924},{5996,3464},{3464,5996}
|
|
},
|
|
/*5.0*/
|
|
{
|
|
{10666,0},{7537,7537},{0,10666},{9234,5331},{5331,9234}
|
|
},
|
|
/*5.1*/
|
|
{
|
|
{8668,0},{6129,6129},{0,8668},{7507,4335},{4335,7507},{6129,6129}
|
|
},
|
|
/*6.1*/
|
|
{
|
|
{7459,0},{5275,5275},{0,7459},{6460,3731},{3731,6460},{4568,4568},
|
|
{5275,5275}
|
|
},
|
|
/*7.1*/
|
|
{
|
|
{6368,0},{4502,4502},{0,6368},{5515,3183},{3183,5515},{5515,3183},
|
|
{3183,5515},{4502,4502}
|
|
}
|
|
};
|
|
|
|
int op_read(OggOpusFile *_of,opus_int16 *_pcm,int _buf_size,int *_li){
|
|
return op_read_native(_of,_pcm,_buf_size,_li);
|
|
}
|
|
|
|
static int op_stereo_filter(OggOpusFile *_of,void *_dst,int _dst_sz,
|
|
op_sample *_src,int _nsamples,int _nchannels){
|
|
(void)_of;
|
|
_nsamples=OP_MIN(_nsamples,_dst_sz>>1);
|
|
if(_nchannels==2)memcpy(_dst,_src,_nsamples*2*sizeof(*_src));
|
|
else{
|
|
opus_int16 *dst;
|
|
int i;
|
|
dst=(opus_int16 *)_dst;
|
|
if(_nchannels==1){
|
|
for(i=0;i<_nsamples;i++)dst[2*i+0]=dst[2*i+1]=_src[i];
|
|
}
|
|
else{
|
|
for(i=0;i<_nsamples;i++){
|
|
opus_int32 l;
|
|
opus_int32 r;
|
|
int ci;
|
|
l=r=0;
|
|
for(ci=0;ci<_nchannels;ci++){
|
|
opus_int32 s;
|
|
s=_src[_nchannels*i+ci];
|
|
l+=OP_STEREO_DOWNMIX_Q14[_nchannels-3][ci][0]*s;
|
|
r+=OP_STEREO_DOWNMIX_Q14[_nchannels-3][ci][1]*s;
|
|
}
|
|
/*TODO: For 5 or more channels, we should do soft clipping here.*/
|
|
dst[2*i+0]=(opus_int16)OP_CLAMP(-32768,l+8192>>14,32767);
|
|
dst[2*i+1]=(opus_int16)OP_CLAMP(-32768,r+8192>>14,32767);
|
|
}
|
|
}
|
|
}
|
|
return _nsamples;
|
|
}
|
|
|
|
int op_read_stereo(OggOpusFile *_of,opus_int16 *_pcm,int _buf_size){
|
|
return op_filter_read_native(_of,_pcm,_buf_size,op_stereo_filter,NULL);
|
|
}
|
|
|
|
# if !defined(OP_DISABLE_FLOAT_API)
|
|
|
|
static int op_short2float_filter(OggOpusFile *_of,void *_dst,int _dst_sz,
|
|
op_sample *_src,int _nsamples,int _nchannels){
|
|
float *dst;
|
|
int i;
|
|
(void)_of;
|
|
dst=(float *)_dst;
|
|
if(OP_UNLIKELY(_nsamples*_nchannels>_dst_sz))_nsamples=_dst_sz/_nchannels;
|
|
_dst_sz=_nsamples*_nchannels;
|
|
for(i=0;i<_dst_sz;i++)dst[i]=(1.0F/32768)*_src[i];
|
|
return _nsamples;
|
|
}
|
|
|
|
int op_read_float(OggOpusFile *_of,float *_pcm,int _buf_size,int *_li){
|
|
return op_filter_read_native(_of,_pcm,_buf_size,op_short2float_filter,_li);
|
|
}
|
|
|
|
static int op_short2float_stereo_filter(OggOpusFile *_of,
|
|
void *_dst,int _dst_sz,op_sample *_src,int _nsamples,int _nchannels){
|
|
float *dst;
|
|
int i;
|
|
dst=(float *)_dst;
|
|
_nsamples=OP_MIN(_nsamples,_dst_sz>>1);
|
|
if(_nchannels==1){
|
|
_nsamples=op_short2float_filter(_of,dst,_nsamples,_src,_nsamples,1);
|
|
for(i=_nsamples;i-->0;)dst[2*i+0]=dst[2*i+1]=dst[i];
|
|
}
|
|
else if(_nchannels<5){
|
|
/*For 3 or 4 channels, we can downmix in fixed point without risk of
|
|
clipping.*/
|
|
if(_nchannels>2){
|
|
_nsamples=op_stereo_filter(_of,_src,_nsamples*2,
|
|
_src,_nsamples,_nchannels);
|
|
}
|
|
return op_short2float_filter(_of,dst,_dst_sz,_src,_nsamples,2);
|
|
}
|
|
else{
|
|
/*For 5 or more channels, we convert to floats and then downmix (so that we
|
|
don't risk clipping).*/
|
|
for(i=0;i<_nsamples;i++){
|
|
float l;
|
|
float r;
|
|
int ci;
|
|
l=r=0;
|
|
for(ci=0;ci<_nchannels;ci++){
|
|
float s;
|
|
s=(1.0F/32768)*_src[_nchannels*i+ci];
|
|
l+=OP_STEREO_DOWNMIX[_nchannels-3][ci][0]*s;
|
|
r+=OP_STEREO_DOWNMIX[_nchannels-3][ci][1]*s;
|
|
}
|
|
dst[2*i+0]=l;
|
|
dst[2*i+1]=r;
|
|
}
|
|
}
|
|
return _nsamples;
|
|
}
|
|
|
|
int op_read_float_stereo(OggOpusFile *_of,float *_pcm,int _buf_size){
|
|
return op_filter_read_native(_of,_pcm,_buf_size,
|
|
op_short2float_stereo_filter,NULL);
|
|
}
|
|
|
|
# endif
|
|
|
|
#else
|
|
|
|
# if defined(OP_HAVE_LRINTF)
|
|
# include <math.h>
|
|
# define op_float2int(_x) (lrintf(_x))
|
|
# else
|
|
# define op_float2int(_x) ((int)((_x)+((_x)<0?-0.5F:0.5F)))
|
|
# endif
|
|
|
|
/*The dithering code here is adapted from opusdec, part of opus-tools.
|
|
It was originally written by Greg Maxwell.*/
|
|
|
|
static opus_uint32 op_rand(opus_uint32 _seed){
|
|
return _seed*96314165+907633515&0xFFFFFFFFU;
|
|
}
|
|
|
|
/*This implements 16-bit quantization with full triangular dither and IIR noise
|
|
shaping.
|
|
The noise shaping filters were designed by Sebastian Gesemann, and are based
|
|
on the LAME ATH curves with flattening to limit their peak gain to 20 dB.
|
|
Everyone else's noise shaping filters are mildly crazy.
|
|
The 48 kHz version of this filter is just a warped version of the 44.1 kHz
|
|
filter and probably could be improved by shifting the HF shelf up in
|
|
frequency a little bit, since 48 kHz has a bit more room and being more
|
|
conservative against bat-ears is probably more important than more noise
|
|
suppression.
|
|
This process can increase the peak level of the signal (in theory by the peak
|
|
error of 1.5 +20 dB, though that is unobservably rare).
|
|
To avoid clipping, the signal is attenuated by a couple thousandths of a dB.
|
|
Initially, the approach taken here was to only attenuate by the 99.9th
|
|
percentile, making clipping rare but not impossible (like SoX), but the
|
|
limited gain of the filter means that the worst case was only two
|
|
thousandths of a dB more, so this just uses the worst case.
|
|
The attenuation is probably also helpful to prevent clipping in the DAC
|
|
reconstruction filters or downstream resampling, in any case.*/
|
|
|
|
# define OP_GAIN (32753.0F)
|
|
|
|
# define OP_PRNG_GAIN (1.0F/0xFFFFFFFF)
|
|
|
|
/*48 kHz noise shaping filter, sd=2.34.*/
|
|
|
|
static const float OP_FCOEF_B[4]={
|
|
2.2374F,-0.7339F,-0.1251F,-0.6033F
|
|
};
|
|
|
|
static const float OP_FCOEF_A[4]={
|
|
0.9030F,0.0116F,-0.5853F,-0.2571F
|
|
};
|
|
|
|
static int op_float2short_filter(OggOpusFile *_of,void *_dst,int _dst_sz,
|
|
float *_src,int _nsamples,int _nchannels){
|
|
opus_int16 *dst;
|
|
int ci;
|
|
int i;
|
|
dst=(opus_int16 *)_dst;
|
|
if(OP_UNLIKELY(_nsamples*_nchannels>_dst_sz))_nsamples=_dst_sz/_nchannels;
|
|
# if defined(OP_SOFT_CLIP)
|
|
if(_of->state_channel_count!=_nchannels){
|
|
for(ci=0;ci<_nchannels;ci++)_of->clip_state[ci]=0;
|
|
}
|
|
opus_pcm_soft_clip(_src,_nsamples,_nchannels,_of->clip_state);
|
|
# endif
|
|
if(_of->dither_disabled){
|
|
for(i=0;i<_nchannels*_nsamples;i++){
|
|
dst[i]=op_float2int(OP_CLAMP(-32768,32768.0F*_src[i],32767));
|
|
}
|
|
}
|
|
else{
|
|
opus_uint32 seed;
|
|
int mute;
|
|
seed=_of->dither_seed;
|
|
mute=_of->dither_mute;
|
|
if(_of->state_channel_count!=_nchannels)mute=65;
|
|
/*In order to avoid replacing digital silence with quiet dither noise, we
|
|
mute if the output has been silent for a while.*/
|
|
if(mute>64)memset(_of->dither_a,0,sizeof(*_of->dither_a)*4*_nchannels);
|
|
for(i=0;i<_nsamples;i++){
|
|
int silent;
|
|
silent=1;
|
|
for(ci=0;ci<_nchannels;ci++){
|
|
float r;
|
|
float s;
|
|
float err;
|
|
int si;
|
|
int j;
|
|
s=_src[_nchannels*i+ci];
|
|
silent&=s==0;
|
|
s*=OP_GAIN;
|
|
err=0;
|
|
for(j=0;j<4;j++){
|
|
err+=OP_FCOEF_B[j]*_of->dither_b[ci*4+j]
|
|
-OP_FCOEF_A[j]*_of->dither_a[ci*4+j];
|
|
}
|
|
for(j=3;j-->0;)_of->dither_a[ci*4+j+1]=_of->dither_a[ci*4+j];
|
|
for(j=3;j-->0;)_of->dither_b[ci*4+j+1]=_of->dither_b[ci*4+j];
|
|
_of->dither_a[ci*4]=err;
|
|
s-=err;
|
|
if(mute>16)r=0;
|
|
else{
|
|
seed=op_rand(seed);
|
|
r=seed*OP_PRNG_GAIN;
|
|
seed=op_rand(seed);
|
|
r-=seed*OP_PRNG_GAIN;
|
|
}
|
|
/*Clamp in float out of paranoia that the input will be > 96 dBFS and
|
|
wrap if the integer is clamped.*/
|
|
si=op_float2int(OP_CLAMP(-32768,s+r,32767));
|
|
dst[_nchannels*i+ci]=(opus_int16)si;
|
|
/*Including clipping in the noise shaping is generally disastrous: the
|
|
futile effort to restore the clipped energy results in more clipping.
|
|
However, small amounts---at the level which could normally be created
|
|
by dither and rounding---are harmless and can even reduce clipping
|
|
somewhat due to the clipping sometimes reducing the dither + rounding
|
|
error.*/
|
|
_of->dither_b[ci*4]=mute>16?0:OP_CLAMP(-1.5F,si-s,1.5F);
|
|
}
|
|
mute++;
|
|
if(!silent)mute=0;
|
|
}
|
|
_of->dither_mute=OP_MIN(mute,65);
|
|
_of->dither_seed=seed;
|
|
}
|
|
_of->state_channel_count=_nchannels;
|
|
return _nsamples;
|
|
}
|
|
|
|
int op_read(OggOpusFile *_of,opus_int16 *_pcm,int _buf_size,int *_li){
|
|
return op_filter_read_native(_of,_pcm,_buf_size,op_float2short_filter,_li);
|
|
}
|
|
|
|
int op_read_float(OggOpusFile *_of,float *_pcm,int _buf_size,int *_li){
|
|
_of->state_channel_count=0;
|
|
return op_read_native(_of,_pcm,_buf_size,_li);
|
|
}
|
|
|
|
static int op_stereo_filter(OggOpusFile *_of,void *_dst,int _dst_sz,
|
|
op_sample *_src,int _nsamples,int _nchannels){
|
|
(void)_of;
|
|
_nsamples=OP_MIN(_nsamples,_dst_sz>>1);
|
|
if(_nchannels==2)memcpy(_dst,_src,_nsamples*2*sizeof(*_src));
|
|
else{
|
|
float *dst;
|
|
int i;
|
|
dst=(float *)_dst;
|
|
if(_nchannels==1){
|
|
for(i=0;i<_nsamples;i++)dst[2*i+0]=dst[2*i+1]=_src[i];
|
|
}
|
|
else{
|
|
for(i=0;i<_nsamples;i++){
|
|
float l;
|
|
float r;
|
|
int ci;
|
|
l=r=0;
|
|
for(ci=0;ci<_nchannels;ci++){
|
|
l+=OP_STEREO_DOWNMIX[_nchannels-3][ci][0]*_src[_nchannels*i+ci];
|
|
r+=OP_STEREO_DOWNMIX[_nchannels-3][ci][1]*_src[_nchannels*i+ci];
|
|
}
|
|
dst[2*i+0]=l;
|
|
dst[2*i+1]=r;
|
|
}
|
|
}
|
|
}
|
|
return _nsamples;
|
|
}
|
|
|
|
static int op_float2short_stereo_filter(OggOpusFile *_of,
|
|
void *_dst,int _dst_sz,op_sample *_src,int _nsamples,int _nchannels){
|
|
opus_int16 *dst;
|
|
dst=(opus_int16 *)_dst;
|
|
if(_nchannels==1){
|
|
int i;
|
|
_nsamples=op_float2short_filter(_of,dst,_dst_sz>>1,_src,_nsamples,1);
|
|
for(i=_nsamples;i-->0;)dst[2*i+0]=dst[2*i+1]=dst[i];
|
|
}
|
|
else{
|
|
if(_nchannels>2){
|
|
_nsamples=OP_MIN(_nsamples,_dst_sz>>1);
|
|
_nsamples=op_stereo_filter(_of,_src,_nsamples*2,
|
|
_src,_nsamples,_nchannels);
|
|
}
|
|
_nsamples=op_float2short_filter(_of,dst,_dst_sz,_src,_nsamples,2);
|
|
}
|
|
return _nsamples;
|
|
}
|
|
|
|
int op_read_stereo(OggOpusFile *_of,opus_int16 *_pcm,int _buf_size){
|
|
return op_filter_read_native(_of,_pcm,_buf_size,
|
|
op_float2short_stereo_filter,NULL);
|
|
}
|
|
|
|
int op_read_float_stereo(OggOpusFile *_of,float *_pcm,int _buf_size){
|
|
_of->state_channel_count=0;
|
|
return op_filter_read_native(_of,_pcm,_buf_size,op_stereo_filter,NULL);
|
|
}
|
|
|
|
#endif
|