diff --git a/src/internal.h b/src/internal.h index 081149199318a2e645a48ea8585418bedc19a23d..ee48ea34c921648fd87bc489e03e2ab1c1d4325f 100644 --- a/src/internal.h +++ b/src/internal.h @@ -186,6 +186,11 @@ struct OggOpusFile{ opus_int32 cur_discard_count; /*The granule position of the previous packet (current packet start time).*/ ogg_int64_t prev_packet_gp; + /*The stream offset of the most recent page with completed packets, or -1. + This is only needed to recover continued packet data in the seeking logic, + when we use the current position as one of our bounds, only to later + discover it was the correct starting point.*/ + opus_int64 prev_page_offset; /*The number of bytes read since the last bitrate query, including framing.*/ opus_int64 bytes_tracked; /*The number of samples decoded since the last bitrate query.*/ diff --git a/src/opusfile.c b/src/opusfile.c index 1acb4205b939894e54a5cc33c2bc36fc99dcad29..18bdbeebac2d4838fa2371496dabe35bd73b152c 100644 --- a/src/opusfile.c +++ b/src/opusfile.c @@ -827,6 +827,7 @@ static opus_int32 op_collect_audio_packets(OggOpusFile *_of, 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; @@ -844,13 +845,12 @@ static int op_find_initial_pcm_offset(OggOpusFile *_of, least once.*/ total_duration=0; do{ - opus_int64 llret; - llret=op_get_next_page(_of,_og,_of->end); + 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(llret<0)){ + if(OP_UNLIKELY(page_offset<0)){ /*Fail if there was a read error.*/ - if(llret<OP_FALSE)return (int)llret; + 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; @@ -951,6 +951,7 @@ static int op_find_initial_pcm_offset(OggOpusFile *_of, _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; } @@ -1404,6 +1405,7 @@ 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; @@ -1423,6 +1425,7 @@ static int op_open_seekable2(OggOpusFile *_of){ 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)); @@ -1439,6 +1442,7 @@ static int op_open_seekable2(OggOpusFile *_of){ 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.*/ @@ -1453,6 +1457,7 @@ static void op_decode_clear(OggOpusFile *_of){ _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); @@ -1817,7 +1822,8 @@ opus_int32 op_bitrate_instant(OggOpusFile *_of){ 0) Need more data (only if _readp==0). 1) Got at least one audio data packet.*/ static int op_fetch_and_process_page(OggOpusFile *_of, - ogg_page *_og,opus_int64 _page_pos,int _readp,int _spanp,int _ignore_holes){ + ogg_page *_og,opus_int64 _page_offset, + int _readp,int _spanp,int _ignore_holes){ OggOpusLink *links; ogg_uint32_t cur_serialno; int seekable; @@ -1845,9 +1851,9 @@ static int op_fetch_and_process_page(OggOpusFile *_of, _og=NULL; } /*Keep reading until we get a page with the correct serialno.*/ - else _page_pos=op_get_next_page(_of,&og,_of->end); + else _page_offset=op_get_next_page(_of,&og,_of->end); /*EOF: Leave uninitialized.*/ - if(_page_pos<0)return _page_pos<OP_FALSE?(int)_page_pos:OP_EOF; + if(_page_offset<0)return _page_offset<OP_FALSE?(int)_page_offset:OP_EOF; if(OP_LIKELY(_of->ready_state>=OP_STREAMSET)){ if(cur_serialno!=(ogg_uint32_t)ogg_page_serialno(&og)){ /*Two possibilities: @@ -1892,8 +1898,9 @@ static int op_fetch_and_process_page(OggOpusFile *_of, _of->ready_state=OP_STREAMSET; /*If we're at the start of this link, initialize the granule position and pre-skip tracking.*/ - if(_page_pos<=links[cur_link].data_offset){ + 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.*/ @@ -2071,6 +2078,7 @@ static int op_fetch_and_process_page(OggOpusFile *_of, 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 1; @@ -2146,7 +2154,7 @@ static ogg_int64_t op_get_granulepos(const OggOpusFile *_of, /*A small helper to determine if an Ogg page contains data that continues onto a subsequent page.*/ -static int op_page_continues(ogg_page *_og){ +static int op_page_continues(const ogg_page *_og){ int nlacing; OP_ASSERT(_og->header_len>=27); nlacing=_og->header[26]; @@ -2156,6 +2164,15 @@ static int op_page_continues(ogg_page *_og){ 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.*/ @@ -2193,7 +2210,7 @@ static int op_pcm_seek_page(OggOpusFile *_of, opus_int64 d1; opus_int64 d2; int force_bisect; - int reset_needed; + int buffering; int ret; _of->bytes_tracked=0; _of->samples_tracked=0; @@ -2203,7 +2220,7 @@ static int op_pcm_seek_page(OggOpusFile *_of, serialno=link->serialno; best=best_start=begin=link->data_offset; page_offset=-1; - reset_needed=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.*/ @@ -2247,11 +2264,20 @@ static int op_pcm_seek_page(OggOpusFile *_of, if(diff<0){ OP_ASSERT(offset>=begin); if(offset-begin>=end-begin>>1||diff>-OP_CUR_TIME_THRESH){ - best=best_start=begin=offset; + best=begin=offset; best_gp=pcm_start=gp; - /*Don't reset the Ogg stream state, or we'll dump any continued - packets from previous pages.*/ - reset_needed=0; + /*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{ @@ -2270,6 +2296,7 @@ static int op_pcm_seek_page(OggOpusFile *_of, _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); } @@ -2290,6 +2317,9 @@ static int op_pcm_seek_page(OggOpusFile *_of, 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; @@ -2315,12 +2345,23 @@ static int op_pcm_seek_page(OggOpusFile *_of, 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){ @@ -2330,7 +2371,10 @@ static int op_pcm_seek_page(OggOpusFile *_of, /*If we scanned the whole interval, we're done.*/ if(bisect<=begin+1)end=begin; else{ - /*Otherwise, back up one chunk.*/ + /*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; @@ -2343,15 +2387,32 @@ static int op_pcm_seek_page(OggOpusFile *_of, } 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; - /*Ignore the granule position on pages where no packets end. - Otherwise we wouldn't properly track continued packets through - them.*/ - gp=ogg_page_packets(&og)>0?ogg_page_granulepos(&og):-1; - if(gp==-1)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.*/ @@ -2364,16 +2425,22 @@ static int op_pcm_seek_page(OggOpusFile *_of, } /*Save the byte offset of the end of the page with this granule position.*/ - best=begin; - /*If this page ends with a continued packet, remember the offset of - its start, so that we can prime the stream with the continued - packet data. - This will likely mean an extra seek backwards, since chances are - we will scan forward at least one more page to figure out where - we're stopping, but continued packets are rare enough it's not - worth the machinery to buffer two pages instead of one to avoid - that seek.*/ - best_start=op_page_continues(&og)?page_offset:best; + 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 @@ -2405,37 +2472,35 @@ static int op_pcm_seek_page(OggOpusFile *_of, } } } - /*Found our page. - The packets we want will end on pages that come after best, but the first - packet might be continued from data in the best page itself. - In that case, best_start will point to the start of the best page, rather - than the end, because that's where we need to start reading. - When there is no danger of a continued packet, best_start==best.*/ - /*Seek, if necessary.*/ - if(best_start!=page_offset){ - page_offset=-1; - ret=op_seek_helper(_of,best_start); - if(OP_UNLIKELY(ret<0))return ret; - } + /*Found our page.*/ OP_ASSERT(op_granpos_cmp(best_gp,pcm_start)>=0); - _of->cur_link=_li; - _of->ready_state=OP_STREAMSET; - /*Update prev_packet_gp to allow per-packet granule position assignment. - If best_start!=best, this is often wrong, but we'll be overwriting it - again shortly.*/ + /*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; - if(reset_needed)ogg_stream_reset_serialno(&_of->os,serialno); + _of->prev_page_offset=best_start; ret=op_fetch_and_process_page(_of,page_offset<0?NULL:&og,page_offset,1,0,1); if(OP_UNLIKELY(ret<=0))return OP_EBADLINK; - if(best_start<best){ - /*The previous call merely primed the stream state to make sure we got the - data for a continued packet. - Now load the packets we actually wanted.*/ - _of->prev_packet_gp=best_gp; - _of->op_pos=_of->op_count; - ret=op_fetch_and_process_page(_of,NULL,-1,1,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;