Opus Audio Codec

Short answer. Opus is the codec behind WebRTC, most real-time voice, and a growing share of archived speech. Two properties break naive decoding. It always works internally at 48 kHz regardless of the input rate, so a 16 kHz source decodes to 48 kHz unless you ask otherwise. And the decoder emits a fixed pre-skip of 312 samples — 6.5 ms at 48 kHz — of lead-in that has to be trimmed, or every timestamp in your pipeline shifts by that amount. Frames are 2.5 to 60 ms, with 20 ms as the default.

How to do it

The internal rate is always 48 kHz

Opus resamples internally to 48 kHz, decodes at 48 kHz, and hands you 48 kHz unless you pass a decode rate. Encode a 16 kHz source, decode it without specifying a rate, and you get 48 kHz output with an 8 kHz bandwidth — a file whose header claims more than its content. If the rest of your corpus is genuine 16 kHz, this file is now out of distribution in a way that no metadata records.

The fix is one parameter on the decoder call, but the failure is silent, which is what makes it worth checking. Verify by reading the sample rate from the decoded output and comparing it to what you intended, on a sample of files rather than one.

Pre-skip is a fixed offset you must trim

The encoder uses lookahead, so the decoder has to run ahead of the input by a fixed number of samples before it can emit the first correct output. Those samples are emitted as part of the stream and are meaningless. The default configuration uses 312 samples at 48 kHz, which is 6.5 ms; some multichannel and very-low-bitrate configurations use 3840 samples, which is 80 ms.

A container such as Ogg stores the pre-skip in the header, and a correct decoder reads it and trims. If you decode raw packets and ignore it, every output file starts 6.5 ms late. That is invisible in a listening test and it is enough to clip the onset of the first phoneme of every segment you cut, and to misalign every forced-alignment timestamp by the same amount.

Frame sizes, and what a lost packet costs

Allowed frame durations are 2.5, 5, 10, 20, 40, and 60 ms. At 48 kHz a 20 ms frame is 960 samples, and that is the default because it is the largest frame that still meets the latency budget for interactive voice. Encoders packetize several frames per packet, typically one to three 20 ms frames, so a packet covers 20 to 60 ms.

Two consequences for a data pipeline. A decoder has to iterate over the frames inside a packet rather than treating the packet as one unit, and packet loss drops 20 to 60 ms of audio rather than a single sample. If you are assembling a corpus from a real-time source, the loss pattern is part of the recording conditions and belongs in the metadata.

Bitrate is a runtime parameter, and the bandwidth moves with it

Opus runs from about 6 kbps to 510 kbps, and the useful range for speech is roughly 6 to 64. At the low end it is using the SILK layer, which is speech-specific and linear-prediction based; at higher rates it switches toward CELT, which is a low-latency transform codec. The two are not the same algorithm at different quality settings, so 16 kbps and 96 kbps are different coding models rather than points on one curve.

The bandwidth scales with the rate as well, and that is the part that matters for training data. The usual operating points are narrowband at 4 kHz for the lowest rates, wideband at 8 kHz in the middle, and fullband at 20 kHz from about 32 kbps up. Everything above 8 kHz is where the fricatives live, so a corpus archived at 16 kbps has had exactly the band an ASR model uses to distinguish "sip" from "ship" removed before the model ever sees it.

Where it belongs in a data pipeline

As an archive format for a large speech corpus, Opus at 32 kbps is 14 MB per hour against 115 MB per hour for 16-bit 16 kHz WAV — an 8x reduction with a bandwidth of 12 kHz, and it is royalty-free and supported everywhere. Whether that trade is worth it depends on the downstream task, and the only way to know is to measure WER on the transcoded corpus rather than to reason about it.

As a training format, the rule is to match the deployment codec. If the product receives Opus from a browser, training on Opus audio at the same bitrate removes a domain mismatch that otherwise costs more than the codec does. If the product receives WAV, archiving in Opus introduces a mismatch for no benefit. The decision follows the deployment, not the storage bill.

Code

The frame and pre-skip facts in numbers, the timestamp shift a naive decode produces, and the bandwidth and storage at five bitrate operating points.

import numpy as np
from scipy.signal import butter, sosfiltfilt

SR, FRAME_MS, PRE_SKIP = 48000, 20, 312

print('20 ms frame at %d Hz: %d samples' % (SR, SR * FRAME_MS // 1000))
print('pre-skip: %d samples = %.2f ms' % (PRE_SKIP, 1000.0 * PRE_SKIP / SR))
print('a packet carrying 3 frames covers %d ms' % (3 * FRAME_MS))
print('at 32 kbps a 20 ms frame carries %d bits, or %.1f bytes'
      % (32000 * FRAME_MS / 1000, 32000 * FRAME_MS / 1000 / 8))

t = np.arange(SR) / SR
x = np.zeros_like(t)
click = int(0.500 * SR)
x[click:click + 5] = 1.0

# A decoder that returns the stream from sample zero without trimming the pre-skip.
naive = np.concatenate([np.zeros(PRE_SKIP), x])[:len(x)]
shift = 1000.0 * (np.argmax(naive) - click) / SR
print()
print('click at %.1f ms in the original; %.1f ms after a naive decode'
      % (1000.0 * click / SR, 1000.0 * np.argmax(naive) / SR))
print('every label in the pipeline is now %.2f ms late' % shift)

f0 = 140.0
speech = sum(np.sin(2 * np.pi * k * f0 * t) / k for k in range(1, 40))
speech = speech * (0.5 + 0.5 * np.sin(2 * np.pi * 3.0 * t)) ** 2
speech = speech + 0.3 * np.random.default_rng(0).normal(size=t.size)
speech = speech / np.max(np.abs(speech))

print()
print('bitrate  bandwidth   energy kept   bits/sample   per hour')
for kbps, cutoff in ((6, 4000), (16, 8000), (32, 12000), (64, 20000), (128, 20000)):
    sos = butter(8, cutoff / (SR / 2.0), btype='low', output='sos')
    band = sosfiltfilt(sos, speech)
    kept = 100.0 * (band ** 2).sum() / (speech ** 2).sum()
    print('%4d kbps  %5d Hz   %6.1f%%        %5.2f      %5.1f MB'
          % (kbps, cutoff, kept, kbps * 1000.0 / SR, kbps * 3600.0 / 8 / 1000))
  • pip install numpy scipy. Nothing is encoded: the script demonstrates the frame arithmetic, the pre-skip offset, and the bandwidth consequence of each bitrate.
  • Expected output: the frame lines print 960 samples, 6.50 ms, 60 ms, and 80 bytes. The click shifts to 506.5 ms, which is the 6.5 ms offset applied to every timestamp downstream.
  • The bandwidth table filters a synthetic voiced signal with a broadband fricative-like component, so the energy-kept column falls from about 60% at 6 kbps to about 90% at 64 kbps. The numbers depend on how much high-frequency energy your audio has; rerun with your own signal to get yours.
  • The bitrate-to-bandwidth pairs are the usual Opus operating points, not a specification. A given encoder may choose a different bandwidth at the same bitrate; read the actual bandwidth from your decoder rather than assuming it.

Where this goes wrong

Decoding without trimming the pre-skip

The default 312 samples at 48 kHz is 6.5 ms of lead-in that is not part of the recording. Ignore it and every output file is shifted by that amount: the first phoneme of every segment is clipped, forced-alignment timestamps are uniformly late, and any cross-correlation against a reference gives a 6.5 ms lag that looks like a hardware delay. The value is in the container header; read it and trim, or use a decoder that does.

Ignoring the decode sample rate

Opus decodes to 48 kHz by default, so a 16 kHz source comes back as a 48 kHz file whose content stops at 8 kHz. Downsampling it back to 16 kHz restores the sample count but not the header story, and if you skip the downsample the corpus now mixes rates invisibly. Set the decode rate explicitly and verify it on the output rather than trusting the call.

Treating one packet as one frame

Encoders pack one to three frames into a packet, so a decoder that treats each packet as a single 20 ms frame produces a stream that runs at one third or one half of the correct duration, or fails outright. Iterate the frames inside the packet. This shows up as audio that plays too fast and as a duration column that does not match the file size, which is easy to miss if nobody checks total hours.

Archiving at a low bitrate and assuming the model will cope

At 6 kbps the bandwidth is narrowband, which means everything above 4 kHz is gone. Fricatives and sibilants live there, and they are what distinguishes many English word pairs. The loss is not noise the model can learn around — the information is absent. Measure WER on the transcoded corpus before committing a large archive to it, and measure on fricative-heavy and noisy subsets, which degrade first.

Assuming a specified bitrate is what you get

The bitrate is a target and the encoder varies it frame to frame, spending more on complex passages and less on silence. A file labeled 32 kbps has a variable instantaneous rate and an average near 32. Storage planning from the average is fine; anything that assumes a fixed frame size, such as a fixed-size packet parser, is not.

When to buy the data instead of building the pipeline

Buy when you need audio that matches the codec your product actually receives, at the same bitrate and the same bandwidth, because that mismatch is worth more accuracy than the storage saving is worth money. Buy when you need the loss and jitter pattern of a real real-time stream, since a clean re-encode has none of it and a model that has only seen clean Opus will not have seen a dropped packet. Buy when you need metadata that records the original rate, the bitrate, and the container, because a corpus assembled from multiple sources will otherwise mix all three invisibly. If you control the recording, archive lossless and transcode a copy for the deployment codec — that direction is always available, and the reverse is not.

How buying training data works →

More technical reference

All technical reference →

Need data for Opus Audio Codec?

Tell us the language, the hours, and what the data needs to look like. You will get a real number and a real timeline — not a range. If we cannot source it well, we will tell you that instead.

  • Pilot batch before the full run, so problems surface early.
  • Consent documentation delivered with the data.
  • No medical or clinical data. No recorded telephone calls.

We reply within two business days. Your details are used only to answer this request. See our privacy policy.

Contact

Talk to a human

Send a specification and we will come back with a real number and timeline.

Submit a sourcing request

Or email hello@linguacorpus.com