davidmweber / scopus

Scala interface to the Opus codec

Version Matrix

Build Status Maven Central

Scopus

Scopus is a Scala interface to the Opus 1.3.1 and g.711u codecs. There is also a PCM "codec" which is effectively a NULL codec and is useful in testing.

It is light and thin by design and gives programmers access to the bulk of the functionality to the codecs. It uses JNI to handle the native code and works for Linux (amd64 and i386) and OSX. Benchmarks performed on the encoder show that it is 10% slower than a native C implementation. For example, on a 3.5Ghz i5, the Opus coder runs at 360 times real time (complexity factor set to 2). The Opus decoder runs at around 1600 times real time. A native benchmark shows the encoder to run at 400 times real time. The LLVM C compiler (clang v3.4) pips GCC by about 5%.

The sources for Opus can be downloaded here.

Licensing

Scopus by David Weber. Released under the Apache-2.0 license.

Building

Clone the sources and do a sbt test. Package and deploy as you see fit.

The native libraries can be built using the makefiles in the src/native/opus directory. You do need to have the Opus libraries and headers installed and copied to the appropriate native directories in the resource directory. You may have to customise the paths to the include file (jni.h) a little. The libraries libjni-opus.{so,dylib} and libopus.{so.0,0.dylib} should be placed in the appropriate resources/native/ directory. Some resources to understanding this process are:

  • Benson Margulies' JNI test bed shows how to avoid LD_LIBRARY_PATH hell.
  • Read the ld man page, especially the bit about -rpath=$ORIGIN (Linux) and @loader_path (OSX). It makes the magic work.
  • For OSX, otool and install_name_tool are your friends.

This library explicitly copies the dynamic libraries out of the resources directory in the jar file to a unique location (something like /tmp/scopus_418af7c0b63 on Linux systems) from where it loads them. The temporary files are deleted when the JVM exits. Note that if you have existing installations of Opus, the linker will likely find that library rather than the bundled one (demonstrated for Linux). This may cause the test cases to fail.

The Opus codec performance improves if compiled with clang. Configure, build and test it as follows:

CC=clang CFLAGS=-O3 ./configure
make
make check

Usage

Scopus is available from the Sonatype Maven repo with builds for Scala 2.12 and 2.13. Add the following dependency to your sbt build:

  libaryDependencies +=  "za.co.monadic" %% "scopus" % "0.5.0"

Encoding a stream is pretty simple. Return types are Scala are wrapped in a Try[_] so it is up to you to manage errors reported by the decoder or the encoder.

   // Variable bit rate Opus encoder with discontinuous transmission
   val enc = OpusEncoder(Sf8000, 1, Audio).setUseDtx(1).setVbr(1)  

   // Corresponding decoder
   val dec = OpusDecoder(Sf8000, 1)

   val coded: Try[Array[Byte]] = enc(new Array[Short](160))
   // Transmit

   // On receive end
   val decoded: Try[Array[Short]] = dec(coded.get)

   // Send decoded packet off

There is now support for a G711 u-law encoder. The standard applies to 8kHz signals but this codec will upsample and downsample the signal to achieve particular sampling rate goals. If the encoder is set to decode to a 48kHz sampling rate, it expects an 8kHz u-Law codec signal which it first decodes to a linear signal then upsamples it to reach 48kHz. Similarly, a decoder configured for 48kHz will first downsample the signal to 8kHz then u-Law encode it. Stereo is not supported for this mode.

There are restrictions on the size of the input buffer. For Opus, audio frames may be one of the following durations: 2.5, 5, 10, 20, 40 or 60 ms. Smaller values obviously give less delay but at the expense of slightly less efficient compression. Note that Java is big endian while most raw audio data are little endian (at least on Intel Architectures). This means you may have to do some byte swapping when reading audio streams from external sources.

Individual instances of encoders and decoders are not thread safe as they have to be called sequentially to maintain the state of the encoder/decoder. The Opus codec instances use a shared decode buffer which is not thread safe. In practice, if you are calling decode on the same instance of the codec, you are managing the stream incorrectly.

Scala does not seem to have a convention for error handling. I went with Try. If this is not how you think it should be done, read the link and make a case. Try can be flatmapped which is important in my application.

Future plans include adding codecs and abstracting the codec layer so they are more pluggable. Pull requests welcome.