Skip to content

Latest commit

 

History

History

ctOpus

README.md

ctOpus: Opus, bit for bit, one file on many cores

ctOpus is an Opus encoder for Windows and Linux (x64) that writes exactly the same .opus files as opusenc (opus-tools 0.2 with libopusenc 0.2.1 and libopus 1.6.1), byte for byte, and spreads the encoding of a single file over all cores. It is a new C++ port (rewritten, not linked) of the Opus encoder and of the opusenc frontend, built on ThinkMeta.ConcurrentTasks, a fiber-based task scheduler for Windows and Linux (x64).

ctOpus was created AI-assisted, using ThinkMeta.ConcurrentTasks: AI coding agents wrote the port, its parallelization and optimizations, measured and tuned the code under the direction of ThinkMeta's developers, and every step was checked byte for byte against the reference. An in-depth code review is still pending: the output is verified to be identical to the reference, but the code itself has not yet been reviewed line by line.

One file, 254 s of 44.1 kHz stereo, 128 kbit/s time vs opusenc --comp 0
opusenc --comp 0 0.99 s 1.00×
opusenc --comp 10 2.21 s 0.45×
ctOpus --comp 10 --sequential (one thread) 2.00 s 0.50×
ctOpus --comp 10 0.50 s 2.0×
ctOpus --comp 0 0.31 s 3.2×

Intel Core i9-11900K (8 cores, 16 threads), Windows 11, High Performance power plan, all variants interleaved in one run, median of 9, every output bit-identical to opusenc with the same settings.

ctOpus at maximum complexity is twice as fast as opusenc at minimum complexity, on a single file and with output identical to opusenc at maximum complexity.

On a corpus of 343 music files (45.1 hours), --comp 10 --bitrate 128, measured cold: 4.4× faster than opusenc on an 8-core Core i9-11900K (559× real time) and 4.1× on a 4-core Xeon E3-1275 v6, all 343 files bit-identical on every machine (details).


Repository contents

Folder / file Contents
bin\win-x64\ Windows: ctOpus.exe (encoder, same command line as opusenc), opusenc.exe (the reference, built from opus\), ThinkMeta.ConcurrentTasks.Core.dll (scheduler core) and two extras: ctOpusGui.exe (live encoding and multi-stream load test) and ctOpusLiveBench.exe (live latency benchmark)
bin/linux-x64/ Linux: ctOpus, opusenc and libThinkMeta.ConcurrentTasks.Core.so.2 (the extras are Windows only)
opus\ Unmodified sources of the reference: opus-tools 0.2, libopusenc 0.2.1, libogg 1.3.6 and libopus 1.6.1, with Visual Studio projects and a CMake build of opusenc for Linux (The reference)
src\ The source code of ctOpus: App\ (the program ctOpus.exe with its opusenc frontend), Encoder\ (the codec, the Ogg container and the WAV reader, a static library without task code), Tasks\ (the task graph), Tests\ (unit tests of the graph and its edges) and the extras Gui\ and LiveBench\; the code all samples share (console output, version resource, CPU check, task setup, --stats) is in ..\Shared (Reading the code)
docs\ task-graph.md (how the graph grew, step by step) and porting.md (which libopus, libopusenc and opus-tools function became which ctOpus function)
ctOpus.slnx Builds everything: ctOpus and its unit tests, the extras ctOpusGui and ctOpusLiveBench (solution folder Extras) and the reference opusenc (Building)
CMakeLists.txt Builds ctOpus and its unit tests on Linux with clang, CMake and Ninja (Building on Linux)
ThinkMeta.SolutionFiles\ Forwards the build settings to the ThinkMeta.ConcurrentTasks.API submodule of this repository
verify\ Verify-Opus.ps1: bit-identity check of a build against opusenc, with synthetic test files and reference hashes, one table per platform (Checking a build)
benchmark\ Compare-Folder.ps1 and run.cmd: speed and bit identity on your own WAV files (Windows only for now)
New-BenchPackage.ps1 Packs bin\win-x64\ and the benchmark into a zip for other PCs (Windows)

Try it

No installation: the programs in bin\ need only the operating system and the scheduler core next to them.

Windows (bin\win-x64\) Linux (bin/linux-x64/)
System Windows 10 or 11, or Windows Server 2016 or later, x64 Linux x64 with glibc 2.38 or later (Ubuntu 24.04, Debian 13, Fedora 39, RHEL 10 or newer); kernel 5.19 or later recommended. ctOpus uses io_uring, which must be allowed (Docker's default seccomp profile blocks it; ctOpus then reports an error at start)
Runtime built with the static C runtime libstdc++ is linked in; the programs need only libc.so.6, libm.so.6 and libThinkMeta.ConcurrentTasks.Core.so.2, which ctOpus finds next to itself

A CPU with AVX2 (Intel from Haswell, 2013; AMD from Excavator and Zen) is needed on both. The encoder is built for AVX2 together with FMA, BMI1/BMI2, LZCNT, MOVBE and F16C; the programs check for all of them, and for an operating system that saves the AVX registers, before anything else runs. Without them they stop with a message instead of crashing: ctOpus and ctOpusLiveBench on the console (exit code 3), ctOpusGui in a message box. opusenc runs on any x64 CPU.

Windows:

bin\win-x64\opusenc --serial 1 --comp 10 input.wav ref.opus
bin\win-x64\ctOpus  --serial 1 --comp 10 input.wav ct.opus
fc /b ref.opus ct.opus

Linux:

bin/linux-x64/opusenc --serial 1 --comp 10 input.wav ref.opus
bin/linux-x64/ctOpus  --serial 1 --comp 10 input.wav ct.opus
cmp ref.opus ct.opus

fc /b and cmp find no differences. (--serial 1 fixes the Ogg stream serial number, which opusenc otherwise picks at random.) ctOpus takes opusenc's command line and prints the same messages, see Command line; on Linux it also reads from standard input (-) and writes to standard output, as opusenc does. If the Linux programs lost their executable bit on the way (an archive unpacked on Windows, for example): chmod +x bin/linux-x64/ctOpus bin/linux-x64/opusenc.

Benchmark on your own files

The benchmark script is Windows only for now; on Linux there is the bit check (verify/) so far.

benchmark\run.cmd "D:\Music"
benchmark\run.cmd "D:\Music" -EncodeArgs "--comp 10" -ReferenceArgs "--comp 0"

The script encodes every file with opusenc and ctOpus, measures the wall time and checks for every file that the ctOpus output is identical to opusenc with the same settings. It switches to the High Performance power plan during the run (or a temporary copy where the plan is hidden) and restores the previous plan afterwards; if Windows refuses the switch, for example on Windows Server without administrator rights, the report says so and the run goes on with the current plan (-KeepPowerPlan leaves the plan alone). The script runs on Windows PowerShell 5.1 and later. Report and CSV go to benchmark\results\; exit code 0 means every file was encoded and is bit-identical, 2 that at least one file differs or was not encoded, 1 other errors (missing programs, bad parameters, abort). Results of your hardware are welcome.


Building

Requirements: Windows 10 or 11 x64 and Visual Studio 2026 with the workload Desktop development with C++. ctOpus's own code is classic headers and .cpp files; the scheduler's library is consumed as C++ modules. The library and the scheduler core come from the submodule ThinkMeta.ConcurrentTasks.API of this repository, so clone it with its submodules:

git clone --recurse-submodules https://github.com/ThinkMeta-com/ThinkMeta.ConcurrentTasks.Samples.git
cd ThinkMeta.ConcurrentTasks.Samples\ctOpus

Clone to a short path such as C:\src; deeply nested folders can exceed the 260-character limit of the build tools.

Open ctOpus.slnx in Visual Studio and build Release | x64, or from a Developer Command Prompt for VS 2026:

msbuild ctOpus.slnx /p:Configuration=Release /p:Platform=x64 /m

Output:

Folder Contents
build\Release\bin\ ctOpus.exe, the unit tests ThinkMeta.ConcurrentTasks.Samples.ctOpus.Tests.dll, the extras ctOpusGui.exe and ctOpusLiveBench.exe, and ThinkMeta.ConcurrentTasks.Core.dll
opus\build\Release\bin\ opusenc.exe, the reference

Your build is not byte-identical to the programs in bin\win-x64\: those are built with the static C runtime and their own build paths, yours uses the C runtime DLL of Visual Studio. What must be identical is the output: every .opus file your ctOpus writes must be bit-identical to opusenc's.

Building on Linux

Requirements: clang 19 or later (tested with clang 20), CMake 3.28 or later and Ninja, as for the C++ modules of the API submodule; on Ubuntu 24.04 sudo apt install cmake ninja-build and a clang from apt.llvm.org (the distribution's clang 18 is too old). ctOpusGui and ctOpusLiveBench are Windows programs and are not built.

git clone --recurse-submodules https://github.com/ThinkMeta-com/ThinkMeta.ConcurrentTasks.Samples.git
cd ThinkMeta.ConcurrentTasks.Samples/ctOpus
cmake -S . -B build/linux-release -G Ninja -DCMAKE_CXX_COMPILER=clang++
cmake --build build/linux-release
ctest --test-dir build/linux-release
cmake -S opus/opus-tools-0.2 -B opus/build/linux-release -G Ninja -DCMAKE_C_COMPILER=clang
cmake --build opus/build/linux-release
Folder Contents
build/linux-release/bin/ ctOpus; it finds the scheduler core in the API submodule (../ThinkMeta.ConcurrentTasks.API/Core/bin/linux-x64)
build/linux-release/ the unit tests of ctOpus and ../Shared, run by ctest
opus/build/linux-release/bin/ opusenc, the reference: opus/opus-tools-0.2/CMakeLists.txt builds the same files with the same defines and config headers as the Visual Studio projects (opus-tools-0.2/linux/config.h in place of msvc/config.h), without FMA contraction

bin/linux-x64/ctOpus links libstdc++ statically and finds the core next to itself (-DCT_SAMPLE_PUBLISH=ON builds it that way). Here, too, your build is not byte for byte that file, but its output must be bit-identical to opusenc's.

Checking a build

verify\Verify-Opus.ps1 (PowerShell 7, on Linux installed from Microsoft's packages) encodes a test matrix with your build and compares every output with the SHA-256 that opusenc of bin\ wrote for it. The inputs are synthetic: verify\New-TestWavs.ps1 computes them on first use (tones, chords, noise, a speech-like pulse train, silence and clipping; mono to 10 channels, 8 to 96 kHz, 8-bit to float) and checks that they come out byte for byte as listed in TestWavs.sha256, on both platforms. Each platform has its own tables (Reference-Hashes-<Matrix>-win-x64.csv, -linux-x64.csv), written by its own opusenc; the script takes the one of the platform it runs on. Their contents are the same: opusenc and ctOpus write the same files on Windows and Linux.

cd verify
.\Verify-Opus.ps1 -Matrix Quick
.\Verify-Opus.ps1 -Matrix Silk
.\Verify-Opus.ps1 -Matrix Surround
.\Verify-Opus.ps1 -Matrix Smoke -ExtraArgs '--threads 4' -Repeat 3
pwsh verify/Verify-Opus.ps1 -Matrix Quick -ThrottleLimit 4
pwsh verify/Verify-Opus.ps1 -Matrix Quick -ThrottleLimit 4 -Exe bin/linux-x64/ctOpus

On Linux run fewer encodes at once than the default (one per logical processor): every ctOpus uses all processors and waits actively between frames, so many parallel runs crowd each other out and some can exceed the time limit of 300 s; -ThrottleLimit 4 avoids that.

OK: all ... encodes bit-identical is the result to expect (exit code 0); any difference is listed, with exit code 1. Quick covers complexities 0 to 10, bit rates, CBR and CVBR, frame sizes 2.5 to 60 ms and the options; Silk the speech modes; Surround 3 to 10 channels; Smoke is a short subset for repeated runs with --threads n or --sequential. The Verify scripts of all samples take the same parameters: -Mode Compare (default) or Reference, -Matrix, -Exe (the program to check, default: your Release build), -Oracle (the reference program), -HashFile, -ExtraArgs (options for -Exe only), -Repeat n (every case n times; all outputs must match), -ThrottleLimit, -WorkDir and -TimeoutSec (a run beyond it counts as a hang). The same check runs against your own WAV files: write a table with opusenc first, then compare.

.\Verify-Opus.ps1 -Mode Reference -Matrix Full -CorpusDir D:\Music -HashFile my-Full.csv
.\Verify-Opus.ps1 -Matrix Full -CorpusDir D:\Music -HashFile my-Full.csv

benchmark\run.cmd checks bit identity as well, file by file, while it measures.

The unit tests run in Visual Studio's Test Explorer or with vstest.console build\Release\bin\ThinkMeta.ConcurrentTasks.Samples.ctOpus.Tests.dll (on Linux with ctest, see above). They need no files (they write their test signals to the temp folder) and check the task graph against the sequential graph and the graph's edges under load (Reading the code).


Benchmarks

Machines, method and corpus

Test machines

Computer CPU Cores / threads Architecture
Desktop Intel Core i9-11900K @ 3.50 GHz 8 / 16 Rocket Lake
Server Intel Xeon E3-1275 v6 @ 3.80 GHz 4 / 8 Kaby Lake
Notebook Intel Core Ultra 7 155H 16 / 22 (6 P + 8 E + 2 LP-E) Meteor Lake

Windows, High Performance power plan during the run (switched by the script where it was not active). The single-file numbers above and the live numbers below are from the desktop.

Method

Measured with benchmark\Compare-Folder.ps1: the wall time of the whole process per file (start, reading, resampling, encoding, Ogg packetization, writing), opusenc and ctOpus one after the other per file, round by round, the median per file; totals are the sums of the medians. Every ctOpus output is compared byte for byte with opusenc with the same settings (--serial 1 for all programs).

Corpus runs: benchmark\run.cmd <folder> -Rounds 3, i.e. --comp 10 --bitrate 128 for both programs, 3 rounds, all threads for ctOpus, measured cold: before every timed run the script writes a fresh copy of the input without buffering, so it is not in the Windows file cache (the copy is not timed).

The corpus numbers were measured on 2026-10-17 with the Windows binaries in bin\win-x64\ (static C runtime); there are no Linux measurements yet. The single-file table at the top and the live numbers below come from earlier builds of both programs, measured warm; they show the same proportions.

Corpus

343 WAV files of music, 45.09 hours (162,329 s) in total, mostly stereo CD rips at 44.1 kHz (resampled to 48 kHz by both programs): 314 files of up to 17 min, mostly single tracks of 3 to 6 min (22.0 hours), and 29 whole albums or concerts in one file of 32 to 79 min each (23.1 hours).

Complexity

Desktop (i9-11900K), whole corpus, 128 kbit/s; time is the sum of the per-file medians:

--comp opusenc ctOpus (all threads) vs opusenc identical
10 1,278.6 s (127× real time) 290.3 s (559× real time) 4.40× 343 / 343

Scaling over the number of threads

ctOpus uses its speculation only from 7 threads on (one level at 7, with its own theta helper; three levels from 8; theta helpers for the three levels from 12); below that the gain comes from the analysis chain, the input stage and, from 3 threads at --comp 8 and above, the theta helper of the encoder.

Other CPUs

Same corpus, --comp 10 --bitrate 128, 3 rounds, cold:

Computer opusenc ctOpus vs opusenc identical
Desktop, i9-11900K, 8 / 16 1,278.6 s (127×) 290.3 s (559×) 4.40× 343 / 343
Server, Xeon E3-1275 v6, 4 / 8 1,787.7 s (91×) 437.5 s (371×) 4.09× 343 / 343
Notebook, Core Ultra 7 155H, 16 / 22 1,430.6 s (113×) 559.5 s (290×) 2.56× 343 / 343
Split by file length: the notebook slows down under sustained load

By file length (the 314 files up to 17 min against the 29 album-length files):

Computer tracks (22.0 h) albums (23.1 h)
Desktop 4.33× 4.48×
Server 4.03× 4.14×
Notebook 3.60× 1.90×

On the desktop and the server the long files scale as well as the tracks. On the notebook the factor falls with the time ctOpus works on one file without a break: 3.6× for a track of 4 minutes (0.7 s), 3.1× at 1.7 s, 2.3× at 3.7 s and about 1.9× for the album files (8 to 17 s), while opusenc on one core even gets slightly faster on them. The most likely cause is the notebook's power limit under sustained load on all cores; that Windows moves the serial encoding task to an efficiency core during long runs would fit as well. Neither has been verified.

Live latency

Live latency: encoding time per frame and many streams in real time

Encoding time per frame for one stream, frames back to back (ctOpusLiveBench --mode single, stereo, 64 kbit/s; p50 / p99 in µs, i9-11900K):

Frame --comp 0 --comp 5 --comp 10
2.5 ms 7.6 / 24 15 / 43 17 / 50
10 ms 21 / 52 36 / 78 48 / 97
20 ms 36 / 71 64 / 114 89 / 164

Many streams in real time (ctOpusLiveBench --mode threads|tasks, 20 ms frames, --comp 10; latency from a frame's hand-over to its packet):

Streams one thread per stream: p50 / p99 / max misses one task per stream: p50 / p99 / max misses
64 0.16 / 0.27 / 1.5 ms 0 0.16 / 0.27 / 1.1 ms 0
512 0.25 / 0.40 / 1.2 ms 0 0.23 / 0.39 / 1.3 ms 0
1,024 0.28 / 0.56 / 30.9 ms 46 0.28 / 0.46 / 10.0 ms 0

Why this is hard

Parallelizing an Opus encoder is easy if you may change its output: cut the audio into pieces, encode them independently and glue the packets together. ctOpus may not. Its output has to be exactly opusenc's, and Opus is sequential by design:

  • every frame continues the range coder of the frame before it; which symbols a frame codes, and how many bits it may spend, depends on how many bits all earlier frames used;
  • the energy quantization predicts each band's energy from the previous frame, and the bit allocation and the VBR reservoir carry over from frame to frame;
  • the filter memories (pre-emphasis, pre-filter, MDCT overlap) and the tonality analysis (a small recurrent network) carry state from frame to frame;
  • at complexity 8 and above the encoder tries rounding choices in the stereo angle search and keeps the cheaper one, so even within a frame the coded result depends on trial encodings.

And "the same output" means the same bits, so every floating point operation must happen in the same order as in libopus, including its SSE code paths and the SSE inner product of the Speex resampler in libopusenc, which sums in a different order than the scalar code.

How it works

FileRead and FileWrite on the I/O scheduler; Read on the compute scheduler feeds Resample ×N; the blocks go to Encode and to the chain Analyse → Signal → Front, which delivers tonality, signal frames and front frames to Encode, with a resync back from Encode; Encode hands trials to Theta and clones of the next frames to Speculate ×3, a cascade in which each level clones the next, with the seed and a cancellation from Encode; Speculate returns symbol logs and has its own Theta helpers; Encode hands its Ogg pages to FileWrite

Rendered with Mermaid's ELK layout; source: docs/images/graph.mmd. The node names are the names in the code (Reading the code).

How the graph grew, step by step → from opusenc's single loop to this graph, with the reason and the measured gain of each step.

Opus is a sequential codec: every frame depends on the state the previous frame left behind (filter memories, energy prediction, bit reservoir, the range coder). ctOpus keeps that sequential backbone exact and takes everything else off it:

  1. Input stage: WAV reading and the Speex resampler of opusenc (e.g. 44.1 → 48 kHz) run in parallel blocks, bit-identical to the streaming resampler. Input at 48 kHz, which opusenc does not resample, goes through the same graph: the resample stage only copies (interleaves) the samples. (Earlier versions encoded 48 kHz input on one thread; going through the graph made it 3.6× faster.)
  2. Analysis chain: tonality analysis, pitch search, transient analysis and the CELT front end (pre-filter, MDCT, band energies) depend only on the signal and run ahead of the encoder on tasks of their own.
  3. Speculative backbone: while the encoder quantizes frame N, up to three speculation tasks already quantize frames N+1 to N+3 on cloned encoder states, with the outcome of the frames before them guessed. The encoder checks every input of the speculation; if it matches, it replays the recorded range coder symbols instead of computing the frame (about 96 % of the speculated frames). A wrong guess is aborted at the next band.
  4. Theta search helpers: at complexity 8 and above (--comp 8; below, CELT makes no second trial), the second rounding candidate of the stereo angle search runs on a helper task at the same time as the first, from 3 threads on.
  5. Bit identity: the same floating point operations in the same order as libopus, including its SSE code paths. Every change is checked against more than 2,400 reference files (all complexities, many bit rates and options, mono, stereo and surround, SILK, hybrid and CELT modes, several sample rates) and against itself with 1 to 16 threads.

The speedup on one file is limited by the backbone that remains sequential; with fewer than 7 threads ctOpus uses no speculation.


The reference

bin\win-x64\opusenc.exe is the unmodified opus-tools 0.2 frontend with libopusenc 0.2.1, libogg 1.3.6 and libopus 1.6.1, built from the sources in opus\ with Visual Studio 2026 for x64 (ctOpus.slnx, projects under opus\): /O2 with whole program optimization, the static C runtime, no /arch switch. Two build choices matter for the bits:

  • libopusenc's Speex resampler is built with __SSE__, i.e. with its SSE inner product, as GCC and Clang builds of opusenc always are on x64 (the official opus-tools release uses the SSE path as well);
  • opus-tools-0.2\msvc\config.h is upstream's win32\config.h without FLAC support.

bin/linux-x64/opusenc is built from the same sources and files with clang (opus/opus-tools-0.2/CMakeLists.txt): the same defines, linux/config.h with the same settings as msvc/config.h, no contraction of a*b+c into FMA (as MSVC), libopus' run-time CPU detection. It writes the same files as bin\win-x64\opusenc.exe.

ctOpus is checked against this build: during development more than 2,400 reference encodes of a music corpus (complexities 0 to 10, bit rates, VBR, CVBR and CBR, frame sizes, mono, stereo and surround up to 10 channels, SILK, hybrid and CELT modes, 8 to 32-bit-float input at 32, 44.1 and 48 kHz) had to give the same SHA-256, with 1 to 16 threads and with --sequential. The corpus is commercial music and cannot be published; verify\ holds the same matrices for computed test signals, more than 1,000 reference encodes (Checking a build).


Command line

ctOpus takes the command line of opusenc 0.2 and prints the same messages and exit codes; ctOpus --help lists everything.

ctOpus [options] <input.wav | -> <output.opus | ->

- stands for standard input or output. Options can be abbreviated as long as they stay unique, and take their value as --opt value or --opt=value, as in opusenc.

opusenc's options, all of which ctOpus accepts
Option Meaning
--bitrate n target bit rate in kbit/s, 6 to 256 per channel; default 64 for mono, 96 for stereo at 44.1 / 48 kHz (lower for lower sample rates)
--vbr, --cvbr, --hard-cbr variable bit rate (default), constrained VBR, hard constant bit rate
--comp n (--complexity n) complexity 0 (fastest) to 10 (slowest, default)
--framesize n maximum frame size in ms: 2.5, 5, 10, 20 (default), 40, 60
--music, --speech tune low bit rates for music or speech (default: automatic)
--expect-loss n expected packet loss in percent, 0 (default) to 100
--downmix-mono, --downmix-stereo, --no-downmix downmix to mono, to stereo (more than 2 channels), or never
--no-phase-inv no phase inversion for intensity stereo
--max-delay n maximum container delay in ms, 0 to 1000 (default)
--title, --artist, --album, --tracknumber, --genre, --date metadata; --artist and --genre may be given several times
--comment tag=value an extra comment, may be given several times
--padding n extra bytes reserved for metadata (default 512)
--discard-comments, --discard-pictures do not keep metadata or pictures of the input
--ignorelength ignore the data length in the WAV header
--serial n Ogg stream serial number (default: random)
--set-ctl-int x=y pass the encoder control x with value y; s:x=y for stream s of a multistream file
--quiet no progress output
-h, --help, --help-picture, -V, --version, --version-short help and version

Options of ctOpus only (not written to the ENCODER_OPTIONS tag, so the output stays identical):

Option Meaning
--threads n number of worker threads; default: all logical processors
--sequential encode on one thread (the reference path)
--stats print the time (process start, setup, encoding with file I/O, rest), the thread count and the peak memory after encoding, in the same line as the other samples

Not supported, with an error message and exit code 1: input other than WAV (AIFF, FLAC, raw PCM, so also --raw, --raw-bits, --raw-rate, --raw-chan, --raw-endianness), --picture and --save-range. WAV input covers the formats opusenc reads (PCM 8, 16 and 24 bit, 32-bit float, WAVE_FORMAT_EXTENSIBLE, any sample rate, up to 255 channels).

The extras ctOpusGui and ctOpusLiveBench: their command lines

ctOpusGui

ctOpusGui.exe is used from its window; the command line is for unattended runs:

Option Meaning
--autostart start the live stream with the default settings (test signal, no playback)
--load n start the multi-stream load test with n streams
--engine i entry i of the encoder list (live: 0 ctOpus, 1 libopus; load test: 0 to 3)
--verify compare every ctOpus packet byte for byte with libopus
--exclusive open the audio devices in exclusive mode (falls back to shared mode)
--playback play the decoded stream on the default output device
--silent with --playback: run the whole playback path, but send silence to the device
--seconds s stop and close after s seconds
--report file write a summary to the file when closing

ctOpusLiveBench

ctOpusLiveBench <input.wav> [options]
Option Meaning
--mode m single: one stream, frames back to back (the encoding time); threads: one OS thread per stream; tasks: one task per stream on the scheduler; resample: self-test of the live resampler. Default tasks
--streams n number of streams in real time (default 64)
--frame ms frame size in ms (default 20)
--comp n complexity (default 10)
--bitrate n bit rate per stream in kbit/s (default 64)
--seconds s length of the run (default 10)
--threads n scheduler threads for tasks (default: all logical processors)
--helper single: use the theta search helper
--gap ms single: pause between the frames (with --spin 0 busy instead of sleeping)
--spin ms threads: busy-wait this long before a frame is due
--csv one CSV line instead of the report

Input for ctOpusLiveBench: 16-bit stereo WAV.


Extras: live encoding (ctOpusGui.exe, ctOpusLiveBench.exe)

The two extras are not part of the file encoder, and you do not need them to follow its task graph. They use the same encoder library (src\Encoder\) and, for the theta search helper, the ThetaHelper of src\Tasks\EncodeHelpers.h; in ctOpus.slnx they are in the solution folder Extras (sources in src\Gui\ and src\LiveBench\).

Live encoding has no look-ahead, so the techniques above do not apply. What counts there is the time from a frame's arrival to its packet:

  • Encoding time per frame (one stream, comp 10, i9-11900K): about 17 µs for 2.5 ms frames and 90 µs for 20 ms frames. The audio device period (3 ms exclusive, 10 ms shared on typical hardware) and the frame length dominate the latency, not the encoder.
  • Many streams: one task per stream on the scheduler is at least as good as one thread per stream; with 1,024 streams of 20 ms frames, tasks had no deadline misses, threads 46 (outliers up to 31 ms).
  • ctOpusGui.exe: live encoding from a microphone, a WAV file or a test signal (WASAPI, shared or exclusive mode), monitoring through the decoder with clock drift compensation, latency breakdown, and a load test with N streams (ctOpus or libopus, tasks or threads).
  • ctOpusLiveBench.exe: command line latency benchmark, e.g. ctOpusLiveBench input.wav --mode tasks --streams 256 --frame 20.

Reading the code

The parallel version adds a task layer (src\Tasks\) around an encoder (src\Encoder\) that does not know about tasks. Read it in this order:

  1. src\Tasks\EncodeGraph.h: the graph as a diagram and the list of its nodes, then EncodeGraph::Initialize and EncodeGraph::Run in EncodeGraph.cpp. Initialize allocates the edges (the PCM feed, the block ring, the rings of the analysis chain). Run decides which helper nodes the graph gets (the thresholds are named at the top of EncodeGraph.cpp: ThetaHelperThreads, ThetaTrialComplexity, OneLevelThreads, CascadeThreads, LevelHelperThreads), creates the nodes, puts them all into one TaskGroup, starts them with ITaskScheduler::AddTask (the serial stages with TaskPriority::High, the resamplers with Normal) and waits for the group. This is where ThinkMeta.ConcurrentTasks is used. SequentialGraph, in the same files, is the one-node graph of --sequential.

  2. The edges, each a class that starts with the nodes it connects:

    • PcmFeed and BlockRing (GraphEdges.h): Read → Resample → Encode and Analyse. The ring's methods name the stage and the step: AcquireRead, CompleteRead, AcquireResample, CompleteResample, AcquireEncode, … (Acquire… may wait and hands out the next block, Complete… hands it on); Abort ends the run and wakes every stage.
    • ToneChain (ToneChain.h): the rings of the analysis chain Analyse → Signal → Front → Encode, and the resync from Encode back to Front. Encode reads them through the encoder's hook interface ToneAnalysisSource.
    • ThetaHelper and SpeculationQueue (EncodeHelpers.h): Encode ⇄ Theta and Encode → Speculate. They implement the encoder's hooks ThetaTrialRunner and SpeculationRunner.
    • BlockPipe (FileIo.h): FileRead → Read and Encode → FileWrite.

    The shared counters are std::atomic. Next to every event its comment says who waits on it: AutoResetEvent_s with "one waiter: …" wherever a single task waits, the plain AutoResetEvent only where several do (the Resample tasks on resampleReady_).

  3. The nodes (EncodeTasks.h, EncodeTasks.cpp): ReadTask, ResampleTask, AnalyseTask, SignalTask, FrontTask, EncodeTask, SpeculateTask, ThetaTask and SequentialEncodeTask; FileReadTask and FileWriteTask are in FileIo.h. Each is a plain loop that waits on an edge.

  4. src\App\Application.cpp: Application::Execute is opusenc's main() after the command line, which CommandLine::Parse (CommandLine.cpp) reads. The setup of ThinkMeta.ConcurrentTasks, the same in every sample, is Shared::TaskRuntime (..\Shared\src\Shared\TaskRuntime.h) with a RunningScheduler for the compute scheduler:

    1. ConvertThreadToTask: the main thread becomes a task, because it waits for the graph's TaskGroup and for the pipes of FileIo. The graph owns its edges and their events; the events themselves are ordinary objects that any thread may set, only waiting needs a task.
    2. EnableAsyncIo: the core's asynchronous I/O (I/O completion port on Windows, io_uring on Linux), for the overlapped reads and writes.
    3. An I/O scheduler with one thread for FileRead and FileWrite, which only wait for the disk.
    4. The compute scheduler, one thread per logical processor, created in RunEncodeGraph once the graph knows its thread count. RunEncodeGraph runs EncodeGraph, or SequentialGraph for --sequential and when EncodeGraph::Initialize fails.
    5. At the end TaskRuntime::End: Stop, DisableAsyncIo and ConvertTaskToThread.

    The programs' entry point is CpuCheckedConsoleStartup (ctOpusGui.exe: CpuCheckedWindowsStartup) in ..\Shared\src\Shared\CpuCheck*.cpp: it checks for AVX2 and the other instruction sets before the C runtime starts and, on an older CPU, names the missing ones and exits with code 3.

  5. The encoder (src\Encoder\), a static library without task code: the port of libopus (OpusStreamEncoder, OpusMultistreamEncoder, CeltEncoder with CeltBands, CeltEnergy, CeltRate, CeltVq, CeltPitch, CeltMdct, KissFft, RangeEncoder, the Silk… files, ToneAnalysis), of libopusenc (OggOpusEncoder, OggPacker, Resampler) and of opus-tools' WAV reader (WavInput). Each ported function names its original in a comment above it, such as // libopus: quant_all_bands() (celt/bands.c); docs/porting.md maps the main functions. New in ctOpus are the parallel resampler (ParallelResampler, PlanarStore), the frames the analysis chain hands over (CeltSignalFrame, CeltFrontFrame, StreamInputFrame) and the speculation (CeltSpeculation.h, CeltSpeculation.cpp). The speculation is a protocol of ctOpus's own, outside the library: a state machine with compare-and-swap and short spins, described with its states and invariants at the top of CeltSpeculation.h. It can cost time, never a bit: a result is taken only if the inputs it read were exactly the real ones.

  6. The tests (src\Tests\), the smallest examples of ThinkMeta.ConcurrentTasks in this repository: EdgeTests.cpp runs BlockRing and BlockPipe under load with stand-in nodes (FunctionTask in TestTasks.h turns a lambda into a task; Shared::TaskRuntime and Shared::RunningScheduler are the setup above in two lines), including an abort in the middle and a consumer that closes early. GraphTests.cpp runs EncodeGraph against SequentialGraph and expects the same Ogg Opus bytes: with 1, 2, 3, 7, 8 and 12 threads (every combination of helper nodes), at 48 kHz (the copy instead of the resampler), mono at 16 kHz, with 6 channels (several streams), for an input shorter than one block and at --comp 5 (no theta helper).

Why some edges are classes of their own and not the library's building blocks:

  • BlockRing, not Semaphore_s: a slot is free only once two stages, Encode and Analyse, have its block, and Read needs the number of the released block, not a count.
  • BlockPipe, not StreamedFileReader and OrderedFileWriter: BlockPipe connects the library's AsyncFile::ReadFile and AsyncFile::WriteFile (overlapped reads and writes on the completion port) with the ported opusenc frontend. The WAV reader pulls its bytes in order and starts behind the header it has already parsed; a read error ends its input, as in opusenc, where StreamedFileReader would read the failed bytes as zeros. The Ogg pages are appended in order and never patched, so they need neither the copy tasks nor the patching of OrderedFileWriter.
  • ThetaHelper, not a plain event: a theta trial takes a few microseconds, so both sides spin first and only then block on an event; the handshake before blocking is described in EncodeHelpers.h.

Error handling

ctOpus reports errors as opusenc does, with its messages and exit code 1, and does not try to recover:

  • Before the encoding (options, the input format, the encoder settings) ctOpus prints opusenc's message and ends the process at once with exit code 1, as opusenc's fatal() does. It does not stop the schedulers first; the end of the process takes them down.
  • A read error ends the input early, as a failing read does in opusenc's WAV reader; the file is encoded up to that point.
  • A write error does not stop the encoder: FileWrite takes the remaining blocks, so the encoder never waits on a full pipe. At the end ctOpus reports Encoding aborted with exit code 1.
  • Memory: if EncodeGraph::Initialize cannot allocate the PCM feed or the block ring, ctOpus encodes with the sequential graph, with the same output; if only the rings of the analysis chain do not fit, the graph runs without the chain (and without speculation). If memory runs out while the graph runs (the input store, the encoder's buffers), the stage that notices aborts the block ring, every stage wakes and ends, and ctOpus reports Encoding aborted. A speculation whose state cannot be allocated is left out; that stream encodes without it.
  • An incomplete output file is not deleted, as with opusenc.

Room for improvement

ctOpus is not finished; the measurements point to these next steps:

  • The sequential backbone: the quantization of the bands (quant_all_bands with the range coder) stays sequential from frame to frame. Speculation runs it ahead, but on the i9-11900K ctOpus still kept only about 3.7 of the 8 cores busy on average (VTune, a build with two speculation levels), and one thread is only 10 % faster than opusenc (2.00 s against 2.21 s at --comp 10).
  • Turbo clock: every additional busy core lowers the clock of all of them. The band quantization itself took about 5 % longer with 2 busy cores and 10 % longer with 6; more speculation levels or helpers therefore pay off only up to a point (three levels from 8 threads, helpers for the levels only from 12).
  • Fewer than 7 threads: below 7 threads speculation costs more than it gains and is switched off, so CPUs with 4 cores / 4 threads get only the analysis chain, the input stage and the theta helper of the encoder.
  • SILK and hybrid modes: the speculation covers CELT frames only; speech at low bit rates (SILK, hybrid) runs the backbone without it. These modes are bit-identical but their speedup has not been measured separately.
  • Notebooks under sustained load: on the Core Ultra 7 155H ctOpus is 3.5× faster than opusenc on single tracks but only 1.9× on album-length files; with fewer threads ctOpus might hold a higher clock for longer. This has not been measured yet.
  • CPUs without AVX2: ctOpus.exe is built for AVX2; a build for older CPUs has not been made or checked.
  • Live encoding: a frame takes 20 to 90 µs, too little to split over cores profitably; the theta helper brought no gain there. A frame that arrives after an idle pause takes 1.4 to 2 times longer than one encoded back to back (the core's caches are cold and its clock is low). Encoding the tonality analysis next to the CELT front end of the same frame would save at most about 15 % at 20 ms frames.
  • Linux: no measurements yet, and the benchmark script is Windows only. Many ctOpus processes at once crowd each other out more than on Windows (the active waits between frames); to be measured. The Linux programs need glibc 2.38 because libstdc++ is linked in from Ubuntu 24.04; a build against an older glibc (in a container) would let them run on Ubuntu 22.04 and Debian 12 as well. The extras ctOpusGui and ctOpusLiveBench are not planned for Linux.
  • Tried and dropped: wider SIMD in the pulse search and the band rotation was bit-identical but slower, because the bands are short; profile-guided optimization gained less than 1 %.

Every step keeps the output bit-identical to opusenc.


ThinkMeta.ConcurrentTasks

Why fibers here

Encoding one file runs about a dozen stages at once: the resampler blocks, the analysis chain that runs ahead of the encoder, up to three speculation levels and the theta helpers. Each is written as a plain sequential loop that waits on an event for its next frame or job; while it waits, its worker thread runs another stage, so the stages share one thread per logical processor instead of needing a thread each. A speculation that turns out wrong is not cancelled from outside: the task checks an abort flag at every band boundary and goes back to waiting for its next frame. For live encoding, one task per stream was measured against one thread per stream (ctOpusLiveBench, 1,024 stereo streams at 64 kbit/s, 20 ms frames, --comp 10, i9-11900K): tasks had no deadline misses, threads 46, with outliers up to 31 ms.

ctOpus is a showcase for ThinkMeta.ConcurrentTasks, a fiber-based task scheduler and concurrency runtime by ThinkMeta Software GmbH for Windows and Linux (x64); ctOpus runs on both. Tasks run on lightweight user-mode fibers: a task waiting for a frame, an event or I/O yields without blocking its worker thread, and a task switch costs a few hundred nanoseconds. That makes it practical to split a frame of 50 to 200 µs into pipeline stages and speculative work.

The library's API (headers, C++ modules and the scheduler core) is in ThinkMeta.ConcurrentTasks.API, included in this repository as the submodule ThinkMeta.ConcurrentTasks.API. The programs in bin\ must be used with the core next to them (ThinkMeta.ConcurrentTasks.Core.dll, libThinkMeta.ConcurrentTasks.Core.so.2): a newer or older core does not run them.

👉 Get in touch: www.thinkmeta.com


License

  • Reference sources and opusenc in bin\ (opus\): BSD-style licenses of Xiph.Org and the Opus contributors (COPYING and the files in each source folder).
  • ctOpus: source code, programs and scripts (src\, ctOpus in bin\win-x64\ and bin/linux-x64/, ctOpusGui.exe, ctOpusLiveBench.exe, verify\, benchmark\, New-BenchPackage.ps1, the build files): MIT License, Copyright (c) 2026 ThinkMeta Software GmbH (LICENSE.txt). ctOpus is a port of libopus, libopusenc and opus-tools; their BSD notices, which apply to the ported code as well, are in COPYING.
  • Scheduler core (ThinkMeta.ConcurrentTasks.Core.dll and libThinkMeta.ConcurrentTasks.Core.so.2 in bin\, and the copies in the ThinkMeta.ConcurrentTasks.API submodule): proprietary, Copyright (c) 2026 ThinkMeta Software GmbH (LICENSE-Core.txt): free for private and non-commercial use, commercial use requires a license from ThinkMeta Software GmbH.

Benchmarks are explicitly permitted: anyone may run benchmark tests of any kind with the programs in bin\ and with programs built from src\, on any hardware and for any purpose, commercial evaluation included, and publish the results, including comparisons with other software, without asking ThinkMeta Software GmbH first (point 2 of LICENSE-Core.txt).