How can there be Clipping in Opus Audio Files opened in Audacity?

39 Views Asked by At

I'm encoding 32-bit floating point Wave (.wav) files to Opus using the command line encoder, opusenc 1.4.

None of the Wave files have clipping (= contain audio samples outside the -1.0 .. +1.0 range), yet:

  • if I open the .opus file produced by the command line encoder in Audacity, there are a handful instances of individual audio samples going under -1.0 or over +1.0.

  • If I instead decode the .opus file via opusdec 1.4 or read it via libopusfile in C++, there is no clipping present in the audio data.

I've compared my code to Audacity's (https://github.com/audacity/audacity/blob/5b9d78bc9ecda8dcb84669541c268cce90b06848/modules/mod-opus/ImportOpus.cpp#L193), both use ::op_read_float().

What is Audacity doing differently? How can the .opus file, opened via drag and drop onto the Audacity window, contain a handful of clipping samples, yet, opusdec and my own code can't replicate the same clipping samples?

1

There are 1 best solutions below

0
Cygon On

I discovered that if I use my system-provided libopus / libopusfile, the clipping issues do occur even outside Audacity.

So there appears to be a small difference in decoding between the official opusdec executable, my debug build of libopus / libopusfile and the riced build of said libraries on my system (compilation flags include -march=native -Ofast -pipe -fomit-frame-pointer -g0 -fgraphite-identity -fno-common -flto=13 -fmerge-all-constants -falign-functions=32 -fno-stack-protector -floop-strip-mine -floop-block -ftree-vectorize -floop-interchange -floop-nest-optimize -floop-parallelize-all -fstack-check=no -fno-stack-check -fno-stack-clash-protection).

The following C++ snippet, when linked with my system's libopus, does reproduce the clipping I see in Audacity:

// Uses a class from the 'AudioFile' library to store audio data
// https://github.com/adamstark/AudioFile
std::unique_ptr<AudioFile<float>> loadOpusFile(const std::string &opusFilePath) {
  std::unique_ptr<AudioFile<float>> audioFile = std::make_unique<AudioFile<float>>();

  ::OggOpusFile *opusFile = ::op_open_file(opusFilePath.c_str(), nullptr);

  std::size_t channelCount = ::op_channel_count(opusFile, -1);
  //std::size_t sampleRate = ::op_head(opusFile, -1)->input_sample_rate;

  // Docs: "The <tt>libopusfile</tt> API always decodes files to 48&nbsp;kHz.
  // The original sample rate is not preserved by the lossy compression, ..."
  audioFile->setSampleRate(48000);

  //audioFile->setNumChannels(channelCount);

  std::vector<std::vector<float>> channels;
  channels.resize(channelCount);

  // Docs: "It is recommended that this be large enough for at least 120 ms
  // of data at 48 kHz per channel (5760 samples per channel)"
  std::vector<float> sampleBuffer;
  sampleBuffer.resize(channelCount * 6144);

  //::op_pcm_t pcm;
  for(;;) {

    // Docs: "he channel count cannot be known a priori (reading more samples might
    // advance us into the next link, with a different channel count)"
    int sampleCountPerChannel = ::op_read_float(
      opusFile, sampleBuffer.data(), sampleBuffer.size(), nullptr
    );
    if(sampleCountPerChannel == 0) {
      break;
    }
    if(sampleCountPerChannel < 0) {
      ::op_free(opusFile);
      throw std::runtime_error(u8"Error reading/decoding OPUS file");
    }

    const ::OpusHead *header = ::op_head(opusFile, -1);
    std::size_t currentChannelCount = header->channel_count;
    if(currentChannelCount != channelCount) {
      ::op_free(opusFile);
      throw std::runtime_error(u8"Channel count changes in the middle of OPUS file");
    }

    // Docs: "Multiple channels are interleaved using the
    // <a href="https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810004.3.9">Vorbis
    // channel ordering</a>."
    float *currentSample = sampleBuffer.data();
    for(int sampleIndex = 0; sampleIndex < sampleCountPerChannel; ++sampleIndex) {
      for(int channelIndex = 0; channelIndex < channelCount; ++channelIndex) {
        channels[channelIndex].push_back(*currentSample);
        ++currentSample;
      }
    }
  }

  // Clean up
  ::op_free(opusFile);  

  // No move assignment? But new buffer also needs to be non-const?
  // It would probably be much more efficient to call setAudioBufferSize()
  // and fill the decoded samples directly into the audio buffer.
  audioFile->setAudioBuffer(channels);

  return audioFile;
}