From c0c963e0e5e62027ff1c91cae49c1905fc7001bb Mon Sep 17 00:00:00 2001 From: "Timothy B. Terriberry" <tterribe@xiph.org> Date: Sun, 16 Sep 2012 21:39:09 -0700 Subject: [PATCH] Implement stereo downmixing functions. Move this out of opusfile_example and into the API proper. --- examples/opusfile_example.c | 74 +--------- include/opus/opusfile.h | 106 ++++++++++++++ src/opusfile.c | 285 +++++++++++++++++++++++++++++++----- 3 files changed, 360 insertions(+), 105 deletions(-) diff --git a/examples/opusfile_example.c b/examples/opusfile_example.c index 0e7d214..f9cf7c8 100644 --- a/examples/opusfile_example.c +++ b/examples/opusfile_example.c @@ -13,45 +13,6 @@ #endif #include <opus/opusfile.h> -/*Matrices for downmixing from the supported channel counts to stereo.*/ -static const float DOWNMIX_MATRIX[8][8][2]={ - /*mono*/ - { - {1.F,1.F} - }, - /*stereo*/ - { - {1.F,0.F},{0.F,1.F} - }, - /*3.0*/ - { - {0.5858F,0.F},{0.4142F,0.4142F},{0,0.5858F} - }, - /*quadrophonic*/ - { - {0.4226F,0.F},{0,0.4226F},{0.366F,0.2114F},{0.2114F,0.336F} - }, - /*5.0*/ - { - {0.651F,0.F},{0.46F,0.46F},{0,0.651F},{0.5636F,0.3254F},{0.3254F,0.5636F} - }, - /*5.1*/ - { - {0.529F,0.F},{0.3741F,0.3741F},{0.F,0.529F},{0.4582F,0.2645F}, - {0.2645F,0.4582F},{0.3741F,0.3741F} - }, - /*6.1*/ - { - {0.4553F,0.F},{0.322F,0.322F},{0.F,0.4553F},{0.3943F,0.2277F}, - {0.2277F,0.3943F},{0.2788F,0.2788F},{0.322F,0.322F} - }, - /*7.1*/ - { - {0.3886F,0.F},{0.2748F,0.2748F},{0.F,0.3886F},{0.3366F,0.1943F}, - {0.1943F,0.3366F},{0.3366F,0.1943F},{0.1943F,0.3366F},{0.2748F,0.2748F} - } -}; - int main(int _argc,const char **_argv){ OggOpusFile *of; ogg_int64_t pcm_offset; @@ -75,7 +36,7 @@ int main(int _argc,const char **_argv){ } if(strcmp(_argv[1],"-")==0){ OpusFileCallbacks cb={NULL,NULL,NULL,NULL}; - of=op_open_callbacks(op_fdopen(&cb,fileno(stdin),"rb"),&cb,NULL,0,NULL); + of=op_open_callbacks(op_fdopen(&cb,fileno(stdin),"rb"),&cb,NULL,0,&ret); } #if 0 /*For debugging: force a file to not be seekable.*/ @@ -88,10 +49,10 @@ int main(int _argc,const char **_argv){ of=op_open_callbacks(fp,&cb,NULL,0,NULL); } #else - else of=op_open_file(_argv[1],NULL); + else of=op_open_file(_argv[1],&ret); #endif if(of==NULL){ - fprintf(stderr,"Failed to open file '%s'.\n",_argv[1]); + fprintf(stderr,"Failed to open file '%s': %i\n",_argv[1],ret); return EXIT_FAILURE; } if(op_seekable(of)){ @@ -109,17 +70,15 @@ int main(int _argc,const char **_argv){ } for(;;){ ogg_int64_t next_pcm_offset; - float pcm[120*48*8]; - float stereo_pcm[120*48*2]; - int nchannels; + float pcm[120*48*2]; int li; - int i; - ret=op_read_float(of,pcm,sizeof(pcm)/sizeof(*pcm),&li); + ret=op_read_float_stereo(of,pcm,sizeof(pcm)/sizeof(*pcm)); if(ret<0){ fprintf(stderr,"Error decoding '%s': %i\n",_argv[1],ret); ret=EXIT_FAILURE; break; } + li=op_current_link(of); if(li!=prev_li){ const OpusHead *head; const OpusTags *tags; @@ -163,26 +122,7 @@ int main(int _argc,const char **_argv){ ret=EXIT_SUCCESS; break; } - /*Downmix to stereo so we can have a consistent output format.*/ - nchannels=op_channel_count(of,li); - if(nchannels<0||nchannels>8){ - fprintf(stderr,"Unsupported channel count: %i\n",nchannels); - ret=EXIT_FAILURE; - break; - } - for(i=0;i<ret;i++){ - float l; - float r; - int ci; - l=r=0.F; - for(ci=0;ci<nchannels;ci++){ - l+=DOWNMIX_MATRIX[nchannels-1][ci][0]*pcm[i*nchannels+ci]; - r+=DOWNMIX_MATRIX[nchannels-1][ci][1]*pcm[i*nchannels+ci]; - } - stereo_pcm[2*i+0]=l; - stereo_pcm[2*i+1]=r; - } - if(!fwrite(stereo_pcm,sizeof(*stereo_pcm)*2,ret,stdout)){ + if(!fwrite(pcm,sizeof(*pcm)*2,ret,stdout)){ fprintf(stderr,"Error writing decoded audio data: %s\n",strerror(errno)); ret=EXIT_FAILURE; break; diff --git a/include/opus/opusfile.h b/include/opus/opusfile.h index 34ebcf0..68fd561 100644 --- a/include/opus/opusfile.h +++ b/include/opus/opusfile.h @@ -784,6 +784,22 @@ const OpusHead *op_head(OggOpusFile *_of,int _li); partially open.*/ const OpusTags *op_tags(OggOpusFile *_of,int _li); +/**Retrieve the index of the current link. + This is the link that produced the data most recently read by + op_read_float() or its associated functions, or, after a seek, the link + that the seek target landed in. + Reading more data may advance the link index (even on the first read after a + seek). + \return The index of the current link on success, or a negative value on + failture. + For seekable streams, this is a number between 0 and the value + returned by op_link_count(). + For unseekable streams, this value starts at 0 and increments by one + each time a new link is encountered (even though op_link_count() + always returns 1). + \retval #OP_EINVAL The stream was not fully open.*/ +int op_current_link(OggOpusFile *_of); + /**Computes the bitrate for a given link in a (possibly chained) Ogg Opus stream. The stream must be seekable to compute the bitrate. @@ -956,6 +972,96 @@ int op_read(OggOpusFile *_of,opus_int16 *_pcm,int _buf_size,int *_li); checks.*/ int op_read_float(OggOpusFile *_of,float *_pcm,int _buf_size,int *_li); +/**Reads more samples from the stream and downmixes to stereo, if necessary. + This function is intended for simple players that want a uniform output + format, even if the channel count changes between links in a chained + stream. + \param[out] _pcm A buffer in which to store the output PCM samples, as + signed native-endian 16-bit values with a nominal + range of <code>[-32768,32767)</code>. + The left and right channels are interleaved in the + buffer. + This must have room for at least \a _buf_size values. + \param _buf_size The number of values that can be stored in \a _pcm. + It is reccommended that this be large enough for at + least 120 ms of data at 48 kHz per channel (11520 + values total). + Smaller buffers will simply return less data, possibly + consuming more memory to buffer the data internally. + \return The number of samples read per channel on success, or a negative + value on failure. + The number of samples returned may be 0 if the buffer was too small + to store even a single sample for both channels, or if end of file + was reached. + The list of possible failure codes follows. + Most of them can only be returned by unseekable, chained streams + that encounter a new link. + \retval #OP_EFAULT An internal memory allocation failed. + \retval #OP_EIMPL An unseekable stream encountered a new link that + used a feature that is not implemented, such as + an unsupported channel family. + \retval #OP_EINVAL The stream was not fully open. + \retval #OP_ENOTFORMAT An unseekable stream encountered a new link that + contained a link that did not have any logical + Opus streams in it. + \retval #OP_EBADHEADER An unseekable stream encountered a new link with a + required header packet that was not properly + formatted, contained illegal values, or was + missing altogether. + \retval #OP_EVERSION An unseekable stream encountered a new link with + an ID header that contained an unrecognized + version number. + \retval #OP_EBADPACKET Failed to properly decode the next packet. + \retval #OP_EBADTIMESTAMP An unseekable stream encountered a new link with + a starting timestamp that failed basic validity + checks.*/ +int op_read_stereo(OggOpusFile *_of,opus_int16 *_pcm,int _buf_size); + +/**Reads more samples from the stream and downmixes to stereo, if necessary. + This function is intended for simple players that want a uniform output + format, even if the channel count changes between links in a chained + stream. + \param[out] _pcm A buffer in which to store the output PCM samples, as + signed floats with a nominal range of + <code>[-1.0,1.0]</code>. + The left and right channels are interleaved in the + buffer. + This must have room for at least \a _buf_size values. + \param _buf_size The number of values that can be stored in \a _pcm. + It is reccommended that this be large enough for at + least 120 ms of data at 48 kHz per channel (11520 + values total). + Smaller buffers will simply return less data, possibly + consuming more memory to buffer the data internally. + \return The number of samples read per channel on success, or a negative + value on failure. + The number of samples returned may be 0 if the buffer was too small + to store even a single sample for both channels, or if end of file + was reached. + The list of possible failure codes follows. + Most of them can only be returned by unseekable, chained streams + that encounter a new link. + \retval #OP_EFAULT An internal memory allocation failed. + \retval #OP_EIMPL An unseekable stream encountered a new link that + used a feature that is not implemented, such as + an unsupported channel family. + \retval #OP_EINVAL The stream was not fully open. + \retval #OP_ENOTFORMAT An unseekable stream encountered a new link that + contained a link that did not have any logical + Opus streams in it. + \retval #OP_EBADHEADER An unseekable stream encountered a new link with a + required header packet that was not properly + formatted, contained illegal values, or was + missing altogether. + \retval #OP_EVERSION An unseekable stream encountered a new link with + an ID header that contained an unrecognized + version number. + \retval #OP_EBADPACKET Failed to properly decode the next packet. + \retval #OP_EBADTIMESTAMP An unseekable stream encountered a new link with + a starting timestamp that failed basic validity + checks.*/ +int op_read_float_stereo(OggOpusFile *_of,float *_pcm,int _buf_size); + # if OP_GNUC_PREREQ(4,0) # pragma GCC visibility pop # endif diff --git a/src/opusfile.c b/src/opusfile.c index 09217f7..4b9f7e1 100644 --- a/src/opusfile.c +++ b/src/opusfile.c @@ -1376,6 +1376,11 @@ const OpusTags *op_tags(OggOpusFile *_of,int _li){ return _li>=_of->nlinks?NULL:&_of->links[_li].tags; } +int op_current_link(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){ @@ -2214,37 +2219,157 @@ static int op_read_native(OggOpusFile *_of, } } -#if defined(OP_FIXED_POINT) +typedef int (*op_read_filter_func)(OggOpusFile *_of,void *_dst,int _dst_sz, + op_sample *_src,int _nsamples,int _nchannels); -int op_read(OggOpusFile *_of,opus_int16 *_pcm,int _buf_size,int *_li){ - return op_read_native(_of,_pcm,_buf_size,_li); -} - -# if !defined(OP_DISABLE_FLOAT_API) -int op_read_float(OggOpusFile *_of,float *_pcm,int _buf_size,int *_li){ +/*Decode some samples and then apply a custom filter to them. + This is used to convert to different output formats.*/ +static int op_read_native_filter(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 convert them to float.*/ + /*Now apply the filter to them.*/ if(OP_LIKELY(ret>=0)&&OP_LIKELY(_of->ready_state>=OP_INITSET)){ - int nchannels; int od_buffer_pos; - nchannels=_of->links[_of->seekable?_of->cur_link:0].head.channel_count; od_buffer_pos=_of->od_buffer_pos; ret=_of->od_buffer_size-od_buffer_pos; if(OP_LIKELY(ret>0)){ - op_sample *buf; - int i; - if(OP_UNLIKELY(ret*nchannels>_buf_size))ret=_buf_size/nchannels; - buf=_of->od_buffer+nchannels*od_buffer_pos; - _buf_size=ret*nchannels; - for(i=0;i<_buf_size;i++)_pcm[i]=(1.0F/32768)*buf[i]; + 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) + +int op_read(OggOpusFile *_of,opus_int16 *_pcm,int _buf_size,int *_li){ + return op_read_native(_of,_pcm,_buf_size,_li); +} + +/*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} + } +}; + +static int op_stereo_filter(OggOpusFile *_of,void *_dst,int _dst_sz, + op_sample *_src,int _nsamples,int _nchannels){ + _of=_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; + } + 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_read_native_filter(_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; + 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_read_native_filter(_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; + dst=(float *)_dst; + _nsamples=OP_MIN(_nsamples,_dst_sz>>1); + if(_nchannels==1){ + int i; + _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]; + return _nsamples; + } + /*It would be better to convert to floats and then downmix (so that we don't + risk clipping with more than 5 channels), but that would require a large + stack buffer, which is probably not a good idea if you're using the + fixed-point build.*/ + 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); +} + +int op_read_stereo_float(OggOpusFile *_of,opus_int16 *_pcm,int _buf_size){ + return op_read_native_filter(_of,_pcm,_buf_size, + op_short2float_stereo_filter,NULL); +} + # endif #else @@ -2297,8 +2422,8 @@ static const float OP_FCOEF_A[4]={ 0.9030F,0.0116F,-0.5853F,-0.2571F }; -static void op_shaped_dither16(OggOpusFile *_of,opus_int16 *_dst,float *_src, - int _nsamples,int _nchannels){ +static void op_shaped_dither16(OggOpusFile *_of,opus_int16 *_dst, + const float *_src,int _nsamples,int _nchannels){ opus_uint32 seed; int mute; int i; @@ -2355,31 +2480,115 @@ static void op_shaped_dither16(OggOpusFile *_of,opus_int16 *_dst,float *_src, _of->dither_seed=seed; } +static int op_float2short_filter(OggOpusFile *_of,void *_dst,int _dst_sz, + op_sample *_src,int _nsamples,int _nchannels){ + opus_int16 *dst; + dst=(opus_int16 *)_dst; + if(OP_UNLIKELY(_nsamples*_nchannels>_dst_sz))_nsamples=_dst_sz/_nchannels; + op_shaped_dither16(_of,dst,_src,_nsamples,_nchannels); + return _nsamples; +} + int op_read(OggOpusFile *_of,opus_int16 *_pcm,int _buf_size,int *_li){ - int ret; - /*Ensure we have some decoded samples in our buffer.*/ - ret=op_read_native(_of,NULL,0,_li); - /*Now convert them to shorts.*/ - if(OP_LIKELY(ret>=0)&&OP_LIKELY(_of->ready_state>=OP_INITSET)){ - int nchannels; - int od_buffer_pos; - nchannels=_of->links[_of->seekable?_of->cur_link:0].head.channel_count; - od_buffer_pos=_of->od_buffer_pos; - ret=_of->od_buffer_size-od_buffer_pos; - if(OP_LIKELY(ret>0)){ - op_sample *buf; - if(OP_UNLIKELY(ret*nchannels>_buf_size))ret=_buf_size/nchannels; - buf=_of->od_buffer+nchannels*od_buffer_pos; - op_shaped_dither16(_of,_pcm,buf,ret,nchannels); - od_buffer_pos+=ret; - _of->od_buffer_pos=od_buffer_pos; - } - } - return ret; + return op_read_native_filter(_of,_pcm,_buf_size,op_float2short_filter,_li); } int op_read_float(OggOpusFile *_of,float *_pcm,int _buf_size,int *_li){ return op_read_native(_of,_pcm,_buf_size,_li); } +/*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} + } +}; + +static int op_stereo_filter(OggOpusFile *_of,void *_dst,int _dst_sz, + op_sample *_src,int _nsamples,int _nchannels){ + _of=_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; + _nsamples=OP_MIN(_nsamples,_dst_sz>>1); + if(_nchannels==1){ + int i; + op_shaped_dither16(_of,dst,_src,_nsamples,1); + for(i=_nsamples;i-->0;)dst[2*i+0]=dst[2*i+1]=dst[i]; + } + else{ + if(_nchannels>2){ + _nsamples=op_stereo_filter(_of,_src,_nsamples*2, + _src,_nsamples,_nchannels); + } + op_shaped_dither16(_of,dst,_src,_nsamples,_nchannels); + } + return _nsamples; +} + +int op_read_stereo(OggOpusFile *_of,opus_int16 *_pcm,int _buf_size){ + return op_read_native_filter(_of,_pcm,_buf_size, + op_float2short_stereo_filter,NULL); +} + +int op_read_float_stereo(OggOpusFile *_of,float *_pcm,int _buf_size){ + return op_read_native_filter(_of,_pcm,_buf_size,op_stereo_filter,NULL); +} + #endif -- GitLab