Codec API and data contract

Copy Markdown View Source

RustyOpus wraps pure-Rust Opus through Rustler.

  • Ogg Opus blobsRustyOpus.reencode/2 demuxes, re-encodes at a numeric bitrate with opus-rs, and remuxes via thin in-crate Ogg glue (ADR003). This is the path for real .ogg / audio/ogg files.
  • Raw Opus packets and PCMencode / decode / transcode and the Encoder / Decoder modules (via opus-rs). No container on this path.
  • WebM/MP4 and ffmpeg are out of scope.

Data contract

  • Ogg Opus is an RFC 7845 binary (starts with OggS).
  • PCM is a binary of 32-bit little-endian IEEE-754 f32 samples, interleaved for stereo. Sample count is byte_size(pcm) / 4.
  • Opus packets are raw binaries, passed verbatim.

Ogg reencode (file-like)

{:ok, smaller} = RustyOpus.reencode(ogg_blob, bitrate: 20_000)

:bitrate is required (bits/s). Typical ladder: 8_00032_000.

Speech-oriented defaults (FFmpeg analogues):

OptionDefaultFFmpeg
:application:voip-application voip
:complexity10-compression_level 10
:cbrfalse (VBR)-vbr on
:frame_duration_ms20-frame_duration (FFmpeg often uses 60)

frame_duration_ms may be 10 or 20. 40 and 60 are not supported by opus-rs at 48 kHz on this path and return :invalid_settings.

Supported configuration (packet/PCM path)

ParameterValues
Sampling rate8000, 12000, 16000, 24000, 48000 Hz
Channels1 (mono) or 2 (stereo)
Application:voip, :audio, :restricted_low_delay

Frame sizes (samples per channel) are the standard Opus frame durations (e.g. 320 samples at 16 kHz is a 20 ms frame). The PCM buffer passed to encode/3 must contain exactly frame_size * channels samples.

Encoder

{:ok, encoder} =
  RustyOpus.Encoder.new(
    48_000,
    2,
    :audio,
    bitrate: 128_000,
    complexity: 9,
    cbr: false,
    fec: false,
    packet_loss: 0
  )

{:ok, packet} = RustyOpus.Encoder.encode(encoder, pcm, 960)
:ok = RustyOpus.Encoder.close(encoder)

RustyOpus.Encoder.set/2 updates settings on a live encoder. Closing is idempotent; calls after close return {:error, %RustyOpus.Error{reason: :closed}}.

Decoder

{:ok, decoder} = RustyOpus.Decoder.new(16_000, 1)
{:ok, pcm} = RustyOpus.Decoder.decode(decoder, packet, 320)

A 1-byte (ToC-only) packet is treated as a lost/DTX frame and concealed with packet-loss concealment instead of erroring. Packets declaring a channel count different from the decoder are rejected with a stable tagged error.

Errors

Every failure is a %RustyOpus.Error{} with a stable :reason and a :message. Panics inside the codec are contained at the NIF boundary and poison the affected resource; they never crash the caller process.

Whole-stream facade

The default path encodes, decodes, or transcodes a whole buffer in one call (20 ms frames by default; a short last encode frame is padded with silence):

{:ok, packets} = RustyOpus.encode(pcm, 16_000, 1, quality: :medium)
{:ok, pcm}     = RustyOpus.decode(packets, 16_000, 1)
{:ok, smaller} = RustyOpus.transcode(packets, 16_000, 1, :low)

Single-frame helpers still create, use, and close a short-lived codec:

{:ok, packet} = RustyOpus.encode_pcm(pcm, 16_000, 1, bitrate: 32_000)
{:ok, pcm} = RustyOpus.decode_packet(packet, 16_000, 1, 320)