Fun with Opus, Part 2

In the last post we looked at how to play an audio clip from a file.

This time around, we're going to use the opus library to compress audio and then decompress it, and see the effects it has on size.

This post was supposed to come last month, but lots of little details came along the way and I couldn't keep it as straightforward as I wanted.

Instead, I give you the second step in a new opusfun repo.

Overall approach

opus is available as a library, so we'll need to figure out how to deal with dependencies.

We could use any number of solutions like vcpkg, but for our pet project we're going to do this with more primitive tools.

We'll simply write all the steps we care for in a build.ps1 PowerShell file.

We're going to do the following.

  1. Clone our dependencies. We'll use the DirectX samples to get an audio file, opus to encode/decode, and we'll grab a helper from opus-tools to resample (more on this later).
  2. Build opus as a static library we can reference from our project.
  3. Copy over the tool files we care about.
  4. Compile and link everything.
  5. Run it!

Setting up the library

There are actually three libraries that we might find interesting.

First let's get the sources at the v1.5.2 tag with git clone https://github.com/xiph/opus && cd opus && git checkout v1.5.2

Next we can setup the CMake build with mkdir build && cd build && cmake .. -G "Visual Studio 17 2022"

And finally, build it with cmake --build .

Note that by default we're going to be building a debug project - this will come back later.

Once the process is complete, you'll find a static library at Debug\opus.lib under your build directory.

Main flow

In play.cpp you will see the following main function, which shows the flow of what we'll be doing - compressing a data file, decompressing it back, then playing it to make sure we didn't mess anything up. File names and such that would normally be parameters are simply globals, to keep this more streamlined.

int main()
{
  HRESULT hr = S_OK;

  IFC(CoInitializeEx(nullptr, COINIT_MULTITHREADED));
  IFC(RunFileCompress());
  IFC(RunFileDecompress());
  IFC(RunFilePlayback());
  CoUninitialize();

Cleanup:
  return FAILED(hr) ? 1 : 0;
}

Let's look at compression first. Because we'll eventually want to use this over the network, we'll be using the encoder directly, so basically following the encoder documentation. This is implemented in the RunFileCompress function, and has the following flow.

  1. Load the audio data with DirectX::LoadWAVAudioFromFileEx.
  2. Check the sample rate. opuslib only works with specific sample rates, and our sample file happens to not be one of them. Instead we're going to use resample the original audio into a supported sample rate. In ResampleWaveData, you can see how we use speex_resampler_process_float (or _int) to do the data conversion.
  3. Create the encoder with opus_encoder_create, and then iterate in frame chunks using opus_encode_float (or _int, again) to turn the pulse code modulation (PCM) representation into opus representation.

As we go along, we write out a file with the sample rate, and then a sequence of packet lengths followed by packet data. This is just something I made up for this sample, instead of using a better-supported format.

Decompressing data

Next up, let's look at decompressing our file and playing it back. We're decompressing to a file rather than on-the-fly, so we don't have to deal with any sort of timing, and the code is again straightforward.

  1. Read the sample rate (we assume channels to be mono here, so we don't deal with whether samples are interleaved or not).
  2. Read each packet (they're length-prefixed), and call opus_decode to turn them into PCM values.
  3. Write everything out to a file.

To write out the file, I made some modifications to WAVFileReader, adding a WriteWAVDataToFile function. The implementation file had all the declarations I needed, so it was easiest to simply append this there. The github file has you covered.

Playing data

Same as the last post where we looked at how to play an audio clip from a file.

Compilation decoder ring

The compilation is done in a single command-line invocation, and it's worth breaking it down. cl.exe /Iopus\include /EHsc /MDd /D "RANDOM_PREFIX=opustools" /D "OUTSIDE_SPEEX" /D "RESAMPLE_FULL_SINC_TABLE" play.cpp WAVFileReader.cpp resample.c /Fe: play.exe /link ole32.lib user32.lib /LIBPATH:opus\build\Debug is a bit too much.

First, note that /link is separating the compiler options that come before from the linker options that come after it.

We could have added opus.lib to the linker flags, but I already had added it via #pragma comment(lib, "opus.lib") when I first included it, so there was no need for that.

All Together

Note that what we've built is basically a baby version of some of the functionality available in the Opus tools themselves.

Next time, we'll take this show on the road and use this to transmit data.

Happy audio compression!

Tags:  audiocmakecpppowershell

Home