forked from len0rd/rockbox
apparently we should be doing this anyway mark4o> The packets overlap and may reuse state set by other recent packets, so if you seek to a different position, resetting the state helps to ensure that the subsequent packets won't use the state set by the unrelated packets that were processed before the seek. remove stack bump WORKAROUND_FS13060 Change-Id: I1c14e23b1721a360b91e3e55202c1557aef0fcc6
528 lines
16 KiB
C
528 lines
16 KiB
C
/***************************************************************************
|
|
* __________ __ ___.
|
|
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
|
|
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
|
|
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
|
|
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
|
|
* \/ \/ \/ \/ \/
|
|
* $Id$
|
|
*
|
|
* Copyright (C) 2012 Frederik M.J. Vestre
|
|
* Based on speex.c codec interface:
|
|
* Copyright (C) 2006 Frederik M.J. Vestre
|
|
* Based on vorbis.c codec interface:
|
|
* Copyright (C) 2002 Björn Stenberg
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
|
|
* KIND, either express or implied.
|
|
*
|
|
****************************************************************************/
|
|
|
|
#include "codeclib.h"
|
|
#include "inttypes.h"
|
|
#include "libopus/opus.h"
|
|
#include "libopus/opus_header.h"
|
|
|
|
|
|
#include "libopus/ogg/ogg.h"
|
|
#ifdef SIMULATOR
|
|
#include <tlsf.h>
|
|
#endif
|
|
|
|
CODEC_HEADER
|
|
|
|
#define SEEK_REWIND 3840 /* 80 ms @ 48 kHz */
|
|
|
|
/* the opus pseudo stack pointer */
|
|
extern char *global_stack;
|
|
|
|
/* Room for 120 ms of stereo audio at 48 kHz */
|
|
#define MAX_FRAME_SIZE (2*120*48)
|
|
#define CHUNKSIZE (16*1024)
|
|
#define SEEK_CHUNKSIZE 7*CHUNKSIZE
|
|
|
|
static int get_more_data(ogg_sync_state *oy)
|
|
{
|
|
int bytes;
|
|
char *buffer;
|
|
|
|
buffer = (char *)ogg_sync_buffer(oy, CHUNKSIZE);
|
|
bytes = ci->read_filebuf(buffer, CHUNKSIZE);
|
|
ogg_sync_wrote(oy,bytes);
|
|
|
|
return bytes;
|
|
}
|
|
|
|
/* seek to ogg page after given file position */
|
|
static int seek_ogg_page(uint64_t filepos)
|
|
{
|
|
const char synccode[] = "OggS\0"; /* Note: there are two nulls here */
|
|
char buf[sizeof(synccode)];
|
|
ci->seek_buffer(filepos);
|
|
while (ci->read_filebuf(buf, 1) == 1) {
|
|
if (buf[0] == synccode[0]) {
|
|
if (ci->read_filebuf(&buf[1], sizeof(buf) - 1) != sizeof(buf) - 1)
|
|
break;
|
|
if (memcmp(buf, synccode, sizeof(buf)) == 0) {
|
|
ci->seek_buffer(ci->curpos - sizeof(buf));
|
|
LOGF("next page %ld", ci->curpos);
|
|
return 1;
|
|
} else
|
|
ci->seek_buffer(ci->curpos - (sizeof(buf) - 1));
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* seek to comment header */
|
|
static int seek_opus_tags(void)
|
|
{
|
|
const char synccode[] = "OpusTags";
|
|
char buf[sizeof(synccode) - 1]; /* Exclude null */
|
|
ci->seek_buffer(0);
|
|
while (ci->read_filebuf(buf, 1) == 1) {
|
|
if (buf[0] == synccode[0]) {
|
|
if (ci->read_filebuf(&buf[1], sizeof(buf) - 1) != sizeof(buf) - 1)
|
|
break;
|
|
if (memcmp(buf, synccode, sizeof(buf)) == 0) {
|
|
ci->seek_buffer(ci->curpos - sizeof(buf));
|
|
LOGF("OpusTags %ld", ci->curpos);
|
|
return 1;
|
|
} else
|
|
ci->seek_buffer(ci->curpos - (sizeof(buf) - 1));
|
|
}
|
|
}
|
|
/* comment header not found probably invalid file */
|
|
return 0;
|
|
}
|
|
|
|
/* The read/seek functions track absolute position within the stream */
|
|
static int64_t get_next_page(ogg_sync_state *oy, ogg_page *og,
|
|
int64_t boundary)
|
|
{
|
|
int64_t localoffset = ci->curpos;
|
|
long more;
|
|
long ret;
|
|
|
|
if (boundary > 0)
|
|
boundary += ci->curpos;
|
|
|
|
while (1) {
|
|
more = ogg_sync_pageseek(oy,og);
|
|
|
|
if (more < 0) {
|
|
/* skipped n bytes */
|
|
localoffset-=more;
|
|
} else {
|
|
if (more == 0) {
|
|
/* send more data */
|
|
if(!boundary)return(-1);
|
|
{
|
|
ret = get_more_data(oy);
|
|
if (ret == 0)
|
|
return(-2);
|
|
|
|
if (ret < 0)
|
|
return(-3);
|
|
}
|
|
} else {
|
|
/* got a page. Return the offset at the page beginning,
|
|
advance the internal offset past the page end */
|
|
|
|
int64_t ret=localoffset;
|
|
|
|
return(ret);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static int64_t seek_backwards(ogg_sync_state *oy, ogg_page *og,
|
|
int64_t wantedpos)
|
|
{
|
|
int64_t crofs;
|
|
int64_t *curoffset=&crofs;
|
|
*curoffset=ci->curpos;
|
|
int64_t begin=*curoffset;
|
|
int64_t end=begin;
|
|
int64_t ret;
|
|
int64_t offset=-1;
|
|
int64_t avgpagelen=-1;
|
|
int64_t lastgranule=-1;
|
|
|
|
short time = -1;
|
|
|
|
while (offset == -1) {
|
|
|
|
begin -= SEEK_CHUNKSIZE;
|
|
|
|
if (begin < 0) {
|
|
if (time < 0) {
|
|
begin = 0;
|
|
time++;
|
|
} else {
|
|
LOGF("Can't seek that early:%lld\n",begin);
|
|
return -3; /* too early */
|
|
}
|
|
}
|
|
|
|
*curoffset = begin;
|
|
|
|
ci->seek_buffer(*curoffset);
|
|
|
|
ogg_sync_reset(oy);
|
|
|
|
lastgranule = -1;
|
|
|
|
while (*curoffset < end) {
|
|
ret = get_next_page(oy,og,end-*curoffset);
|
|
|
|
if (ret > 0) {
|
|
if (lastgranule != -1) {
|
|
if (avgpagelen < 0)
|
|
avgpagelen = (ogg_page_granulepos(og)-lastgranule);
|
|
else
|
|
avgpagelen=((ogg_page_granulepos(og)-lastgranule)
|
|
+ avgpagelen) / 2;
|
|
}
|
|
|
|
lastgranule=ogg_page_granulepos(og);
|
|
|
|
if ((lastgranule - (avgpagelen/4)) < wantedpos &&
|
|
(lastgranule + avgpagelen + (avgpagelen/4)) > wantedpos) {
|
|
|
|
/*wanted offset found Yeay!*/
|
|
|
|
/*LOGF("GnPagefound:%d,%d,%d,%d\n",ret,
|
|
lastgranule,wantedpos,avgpagelen);*/
|
|
|
|
return ret;
|
|
|
|
} else if (lastgranule > wantedpos) { /*too late, seek more*/
|
|
if (offset != -1) {
|
|
LOGF("Toolate, returnanyway:%lld,%lld,%lld,%lld\n",
|
|
ret,lastgranule,wantedpos,avgpagelen);
|
|
return ret;
|
|
}
|
|
break;
|
|
} else{ /*if (ogg_page_granulepos(&og)<wantedpos)*/
|
|
/*too early*/
|
|
offset = ret;
|
|
continue;
|
|
}
|
|
} else if (ret == -3)
|
|
return(-3);
|
|
else if (ret<=0)
|
|
break;
|
|
else if (*curoffset < end) {
|
|
/*this should not be possible*/
|
|
|
|
//LOGF("Seek:get_earlier_page:Offset:not_cached by granule:"\"%d,%d,%d,%d,%d\n",*curoffset,end,begin,wantedpos,curpos);
|
|
|
|
offset=ret;
|
|
}
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static int opus_seek_page_granule(int64_t pos, int64_t curpos,
|
|
ogg_sync_state *oy, ogg_stream_state *os)
|
|
{
|
|
/* TODO: Someone may want to try to implement seek to packet,
|
|
instead of just to page (should be more accurate, not be any
|
|
faster) */
|
|
|
|
int64_t crofs;
|
|
int64_t *curbyteoffset = &crofs;
|
|
*curbyteoffset = ci->curpos;
|
|
int64_t curoffset;
|
|
curoffset = *curbyteoffset;
|
|
int64_t offset = 0;
|
|
ogg_page og = {0,0,0,0};
|
|
int64_t avgpagelen = -1;
|
|
int64_t lastgranule = -1;
|
|
#if 0
|
|
if(abs(pos-curpos)>10000 && headerssize>0 && curoffset-headerssize>10000) {
|
|
/* if seeking for more that 10sec,
|
|
headersize is known & more than 10kb is played,
|
|
try to guess a place to seek from the number of
|
|
bytes playe for this position, this works best when
|
|
the bitrate is relativly constant.
|
|
*/
|
|
|
|
curoffset = (((*curbyteoffset-headerssize) * pos)/curpos)*98/100;
|
|
if (curoffset < 0)
|
|
curoffset=0;
|
|
|
|
//int64_t toffset=curoffset;
|
|
|
|
ci->seek_buffer(curoffset);
|
|
|
|
ogg_sync_reset(oy);
|
|
|
|
offset = get_next_page(oy,&og,-1);
|
|
|
|
if (offset < 0) { /* could not find new page,use old offset */
|
|
LOGF("Seek/guess/fault:%lld->-<-%d,%lld:%lld,%d,%ld,%d\n",
|
|
curpos,0,pos,offset,0,
|
|
ci->curpos,/*stream_length*/0);
|
|
|
|
curoffset = *curbyteoffset;
|
|
|
|
ci->seek_buffer(curoffset);
|
|
|
|
ogg_sync_reset(oy);
|
|
} else {
|
|
if (ogg_page_granulepos(&og) == 0 && pos > 5000) {
|
|
LOGF("SEEK/guess/fault:%lld->-<-%lld,%lld:%lld,%d,%ld,%d\n",
|
|
curpos,ogg_page_granulepos(&og),pos,
|
|
offset,0,ci->curpos,/*stream_length*/0);
|
|
|
|
curoffset = *curbyteoffset;
|
|
|
|
ci->seek_buffer(curoffset);
|
|
|
|
ogg_sync_reset(oy);
|
|
} else {
|
|
curoffset = offset;
|
|
curpos = ogg_page_granulepos(&og);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
/* which way do we want to seek? */
|
|
if (pos == 0) { /* start */
|
|
*curbyteoffset = 0;
|
|
ci->seek_buffer(*curbyteoffset);
|
|
ogg_sync_reset(oy);
|
|
ogg_stream_reset(os);
|
|
return 0;
|
|
} else if (curpos > pos) { /* backwards */
|
|
offset = seek_backwards(oy,&og,pos);
|
|
|
|
if (offset > 0) {
|
|
*curbyteoffset = curoffset;
|
|
return 1;
|
|
}
|
|
} else { /* forwards */
|
|
|
|
while ( (offset = get_next_page(oy,&og,-1)) > 0) {
|
|
if (lastgranule != -1) {
|
|
if (avgpagelen < 0)
|
|
avgpagelen = (ogg_page_granulepos(&og) - lastgranule);
|
|
else
|
|
avgpagelen = ((ogg_page_granulepos(&og) - lastgranule)
|
|
+ avgpagelen) / 2;
|
|
}
|
|
|
|
lastgranule = ogg_page_granulepos(&og);
|
|
|
|
if ( ((lastgranule - (avgpagelen/4)) < pos && ( lastgranule +
|
|
avgpagelen + (avgpagelen / 4)) > pos) ||
|
|
lastgranule > pos) {
|
|
|
|
/*wanted offset found Yeay!*/
|
|
|
|
*curbyteoffset = offset;
|
|
|
|
return offset;
|
|
}
|
|
}
|
|
}
|
|
|
|
ci->seek_buffer(*curbyteoffset);
|
|
|
|
ogg_sync_reset(oy);
|
|
|
|
LOGF("Seek failed:%lld\n", offset);
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
/* this is the codec entry point */
|
|
enum codec_status codec_main(enum codec_entry_call_reason reason)
|
|
{
|
|
(void)reason; /* CODEC_LOAD, CODEC_UNLOAD */
|
|
return CODEC_OK;
|
|
}
|
|
|
|
/* this is called for each file to process */
|
|
enum codec_status codec_run(void)
|
|
{
|
|
int error = CODEC_ERROR;
|
|
long action;
|
|
intptr_t param;
|
|
ogg_sync_state oy;
|
|
ogg_page og;
|
|
ogg_packet op;
|
|
ogg_stream_state os;
|
|
int64_t page_granule = 0;
|
|
int stream_init = -1;
|
|
int sample_rate = 48000;
|
|
OpusDecoder *st = NULL;
|
|
OpusHeader header;
|
|
int ret;
|
|
unsigned long strtoffset;
|
|
int skip = 0;
|
|
int64_t seek_target;
|
|
uint64_t granule_pos;
|
|
|
|
ogg_malloc_init();
|
|
|
|
action = CODEC_ACTION_NULL;
|
|
param = ci->id3->elapsed;
|
|
strtoffset = ci->id3->offset;
|
|
|
|
#if defined(CPU_COLDFIRE)
|
|
/* EMAC rounding is disabled because of MULT16_32_Q15, which will be
|
|
inaccurate with rounding in its current incarnation */
|
|
coldfire_set_macsr(EMAC_FRACTIONAL | EMAC_SATURATE);
|
|
#endif
|
|
|
|
/* pre-init the ogg_sync_state buffer, so it won't need many reallocs */
|
|
ogg_sync_init(&oy);
|
|
oy.storage = 64*1024;
|
|
oy.data = _ogg_malloc(oy.storage);
|
|
|
|
/* allocate output buffer */
|
|
uint16_t *output = (uint16_t*) _ogg_malloc(MAX_FRAME_SIZE*sizeof(uint16_t));
|
|
|
|
ci->seek_buffer(0);
|
|
ci->set_elapsed(0);
|
|
|
|
goto next_page; /* need to decode header before we do anything else */
|
|
|
|
while (1) {
|
|
action = ci->get_command(¶m);
|
|
|
|
process_action:
|
|
if (action == CODEC_ACTION_SEEK_TIME) {
|
|
if (st != NULL) {
|
|
/* calculate granule to seek to (including seek rewind) */
|
|
seek_target = (48LL * param) + header.preskip;
|
|
skip = MIN(seek_target, SEEK_REWIND);
|
|
seek_target -= skip;
|
|
|
|
LOGF("Opus seek page:%lld,%lld,%ld\n",
|
|
seek_target, page_granule, (long)param);
|
|
opus_seek_page_granule(seek_target, page_granule, &oy, &os);
|
|
/* reset the state to help ensure that subsequent packets won't
|
|
use state set by unrelated packets processed before seek */
|
|
opus_decoder_ctl(st, OPUS_RESET_STATE);
|
|
}
|
|
|
|
ci->set_elapsed(param);
|
|
ci->seek_complete();
|
|
param = 0;
|
|
} else if (action == CODEC_ACTION_HALT)
|
|
break;
|
|
|
|
next_page:
|
|
/*Get the ogg buffer for writing*/
|
|
if (get_more_data(&oy) < 1) {
|
|
goto done;
|
|
}
|
|
|
|
/* Loop for all complete pages we got (most likely only one) */
|
|
while (ogg_sync_pageout(&oy, &og) == 1) {
|
|
if (stream_init != 0) {
|
|
stream_init = ogg_stream_init(&os, ogg_page_serialno(&og));
|
|
if (stream_init != 0) {
|
|
LOGF("Stream init failed");
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
/* Add page to the bitstream */
|
|
ogg_stream_pagein(&os, &og);
|
|
|
|
page_granule = ogg_page_granulepos(&og);
|
|
granule_pos = page_granule;
|
|
|
|
while ((ogg_stream_packetout(&os, &op) == 1) && !op.e_o_s) {
|
|
if (op.packetno == 0){
|
|
/* identification header */
|
|
|
|
if (opus_header_parse(op.packet, op.bytes, &header) == 0) {
|
|
LOGF("Could not parse header");
|
|
goto done;
|
|
}
|
|
skip = header.preskip;
|
|
|
|
st = opus_decoder_create(sample_rate, header.channels, &ret);
|
|
if (ret != OPUS_OK) {
|
|
LOGF("opus_decoder_create failed %d", ret);
|
|
goto done;
|
|
}
|
|
LOGF("Decoder inited");
|
|
|
|
codec_set_replaygain(ci->id3);
|
|
|
|
opus_decoder_ctl(st, OPUS_SET_GAIN(header.gain));
|
|
|
|
ci->configure(DSP_SET_FREQUENCY, sample_rate);
|
|
ci->configure(DSP_SET_SAMPLE_DEPTH, 16);
|
|
ci->configure(DSP_SET_STEREO_MODE, (header.channels == 2) ?
|
|
STEREO_INTERLEAVED : STEREO_MONO);
|
|
|
|
if (strtoffset)
|
|
seek_ogg_page(strtoffset);
|
|
else if (param) {
|
|
action = CODEC_ACTION_SEEK_TIME;
|
|
goto process_action;
|
|
} else {
|
|
/* seek buffer directly to the first audio packet to avoid
|
|
allocating space for huge comment packets
|
|
(embedded Album Art) */
|
|
if (seek_opus_tags())
|
|
seek_ogg_page(ci->curpos);
|
|
else {
|
|
LOGF("Could not find comment header");
|
|
goto done;
|
|
}
|
|
}
|
|
LOGF("sync reset");
|
|
ogg_sync_reset(&oy); /* next page */
|
|
break;
|
|
} else {
|
|
/* report progress */
|
|
ci->set_offset((size_t) ci->curpos);
|
|
ci->set_elapsed((granule_pos - header.preskip) / 48);
|
|
|
|
/* Decode audio packets */
|
|
ret = opus_decode(st, op.packet, op.bytes, output, MAX_FRAME_SIZE, 0);
|
|
|
|
if (ret > skip) {
|
|
/* part of or entire output buffer is played */
|
|
ret -= skip;
|
|
ci->pcmbuf_insert(&output[skip * header.channels], NULL, ret);
|
|
skip = 0;
|
|
} else {
|
|
if (ret < 0) {
|
|
LOGF("opus_decode failed %d", ret);
|
|
goto done;
|
|
} else if (ret == 0)
|
|
break;
|
|
else {
|
|
/* entire output buffer is skipped */
|
|
skip -= ret;
|
|
ret = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
LOGF("Returned OK");
|
|
error = CODEC_OK;
|
|
done:
|
|
ogg_malloc_destroy();
|
|
return error;
|
|
}
|