Implement stereo downmixing functions.
authorTimothy B. Terriberry <tterribe@xiph.org>
Mon, 17 Sep 2012 04:39:09 +0000 (21:39 -0700)
committerTimothy B. Terriberry <tterribe@xiph.org>
Mon, 17 Sep 2012 04:39:09 +0000 (21:39 -0700)
Move this out of opusfile_example and into the API proper.

examples/opusfile_example.c
include/opus/opusfile.h
src/opusfile.c

index 0e7d214..f9cf7c8 100644 (file)
 #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;
index 34ebcf0..68fd561 100644 (file)
@@ -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
index 09217f7..4b9f7e1 100644 (file)
@@ -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