ctLame is an MP3 encoder that produces exactly the same files as LAME 4.0 (byte for byte) and runs 14 times as fast on an 8-core desktop CPU. It is a C++ port of LAME's VBR encoder, built on ThinkMeta.ConcurrentTasks, a fiber-based task scheduler for Windows and Linux (x64), and a first look at the research project around it that runs from 2027 to 2029 (what comes next).
ctLame 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.
The source code of ctLame is here (src\, GNU LGPL) and builds with Visual Studio 2026 on
Windows and with clang and CMake on Linux (Building); bin\win-x64\ holds the prebuilt
ctLame.exe the benchmark results below were measured with, bin/linux-x64/ the Linux build.
| lame.exe | ctLame | |
|---|---|---|
45 hours of CD audio (343 files), -V 4 |
19 min 55 s | 85 s |
| speed | 136× real time | 1905× real time |
| output | reference | bit-identical |
This repository contains:
| Folder | Contents |
|---|---|
bin\win-x64\ |
prebuilt Windows x64 binaries: lame.exe (the reduced LAME below), ctLame.exe, and ThinkMeta.ConcurrentTasks.Core.dll (the scheduler core ctLame runs on) |
bin/linux-x64/ |
the same for Linux x64: lame, ctLame and libThinkMeta.ConcurrentTasks.Core.so.2 |
src\ |
the source of ctLame: App (the program, LAME's command line), Tasks (the task graph), Encoder (the port of LAME's encoder, without task code) and Tests (unit tests); the code all samples share (console output, version resource, CPU check, task setup, --stats) is in ..\Shared; see Reading the code |
lame\ |
the source of the reduced LAME 4.0, the reference ctLame is measured against, with Visual Studio projects and a CMake build for Linux |
ctLame.slnx |
a Visual Studio solution that builds ctLame.exe and lame.exe (Building) |
CMakeLists.txt |
the Linux build of ctLame and its unit tests (clang, CMake, Ninja) |
verify\ |
scripts that check a ctLame build byte for byte against lame, with computed test signals and their reference hashes (one table per platform) |
benchmark\ |
a script to compare both encoders on your own WAV files: speed and bit identity (Windows only for now; needs ctLame.exe in bin\win-x64\) |
ThinkMeta.SolutionFiles\ |
build settings; they forward to the ThinkMeta.ConcurrentTasks submodule next to this folder |
No installation and no runtime to install. Any x64 CPU runs ctLame; it chooses its SIMD code (scalar, SSE4.1, AVX2) at run time.
Windows (bin\win-x64\) |
Linux (bin/linux-x64/) |
|
|---|---|---|
| System | Windows 10 or 11, 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. ctLame uses io_uring, which must be allowed (Docker's default seccomp profile blocks it; ctLame then reports an error at start) |
| Runtime | the C runtime is linked statically | libstdc++ is linked in; the programs need only libc.so.6 and libm.so.6, ctLame also libThinkMeta.ConcurrentTasks.Core.so.2, which it finds next to itself |
Windows:
bin\win-x64\lame -V 2 input.wav lame.mp3
bin\win-x64\ctLame -V 2 input.wav ctlame.mp3
fc /b lame.mp3 ctlame.mp3
Linux:
bin/linux-x64/lame -V 2 input.wav lame.mp3
bin/linux-x64/ctLame -V 2 input.wav ctlame.mp3
cmp lame.mp3 ctlame.mp3
fc and cmp report no differences. The command lines are the same, too: ctLame accepts every
option of the reduced LAME and prints the same messages. Both write the ReplayGain of the input into
the LAME tag by default, like unmodified LAME 4.0, and take LAME's ID3 tag options
(Tags and ReplayGain). If the Linux programs lost their
executable bit on the way (an archive unpacked on Windows, for example):
chmod +x bin/linux-x64/lame bin/linux-x64/ctLame.
To measure on your own music on Windows, run benchmark\run.cmd D:\Music (any folder of WAV files; the script is Windows only for now). It encodes
every file with both programs, one process at a time, and writes a report with the times and the
bit-identity check of each file. Both programs run with --noreplaygain, as the published numbers
were measured; -ReplayGain runs both with their default ReplayGain analysis. Every run reads a fresh copy of the file that is not in the file
cache (-Warm reads the files where they are). For the run it switches to the High Performance
power plan and restores the previous one afterwards (-KeepPowerPlan leaves it alone).
You need Visual Studio 2026 with the workload Desktop development with C++ (it brings the
C++ modules support and the native unit test framework the tests use). ctLame is built on the
library in the submodule ThinkMeta.ConcurrentTasks.API, so clone with submodules. Clone to a short
path such as C:\src; deeply nested folders can exceed the 260-character limit of the build tools.
git clone --recurse-submodules https://github.com/ThinkMeta-com/ThinkMeta.ConcurrentTasks.Samples.git
cd ThinkMeta.ConcurrentTasks.Samples\ctLame
msbuild ctLame.slnx /p:Configuration=Release /p:Platform=x64
(or open ctLame.slnx in Visual Studio and build Release | x64). The solution builds the library
modules, the ctLame projects and the reduced LAME. Everything lands in build\Release\bin\:
ctLame.exe with ThinkMeta.ConcurrentTasks.Core.dll next to it, lame.exe, and the unit tests
(ThinkMeta.ConcurrentTasks.Samples.ctLame.Tests.dll, run them in the Test Explorer or with
vstest.console). The golden tests need the WAV files their checksums were recorded with and skip
without them (environment variables CTLAME_TEST_CORPUS and CTLAME_TEST_SYNTH); the
measurements run only with CTLAME_BENCH_FILE or CTLAME_BENCH_EVENTS set.
Your build is not byte-identical to the published binaries: those are built with the static C
runtime, without buffer security checks and with profile-guided optimization (PGO), which makes
ctLame about 4 % faster. None of that changes what it computes: the MP3 files of every build must
be bit-identical to lame.exe's, and verify\ checks exactly that.
You need 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).
git clone --recurse-submodules https://github.com/ThinkMeta-com/ThinkMeta.ConcurrentTasks.Samples.git
cd ThinkMeta.ConcurrentTasks.Samples/ctLame
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 lame -B lame/build/linux-release -G Ninja -DCMAKE_C_COMPILER=clang
cmake --build lame/build/linux-release
ctLame lands in build/linux-release/bin/ (it finds the scheduler core in the API submodule,
../ThinkMeta.ConcurrentTasks.API/Core/bin/linux-x64), the reduced LAME in
lame/build/linux-release/bin/lame: lame/CMakeLists.txt builds the same files with the same
switches as the Visual Studio projects (configLinux.h in place of configMS.h), without FMA
contraction. ctest runs the unit tests of ctLame and ../Shared, with the same environment
variables as above. The published bin/linux-x64/ctLame links libstdc++ statically, finds the core
next to itself (-DCT_SAMPLE_PUBLISH=ON builds it that way) and is profile-guided optimized with
ThinLTO and without stack protector; bin/linux-x64/lame keeps the plain build, as on Windows.
verify\ needs PowerShell 7 (pwsh, on Linux installed from Microsoft's packages), nothing else. Without -Exe and -Oracle the scripts check your Release build of the platform they run on:
pwsh verify\Verify-Lame.ps1 -Signals
pwsh verify\Verify-Lame.ps1 -WavDir D:\Music -Matrix Quick
pwsh verify\Compare-LameCli.ps1
pwsh verify/Verify-Lame.ps1 -Signals
pwsh verify/Verify-Lame.ps1 -Signals -Exe bin/linux-x64/ctLame
pwsh verify/Verify-Lame.ps1 -WavDir ~/Music -Matrix Quick
-Signalsgenerates 20 test signals (New-TestSignals.ps1: tones, sweeps, noise, transients, every supported sample rate and sample format, edge cases) with integer arithmetic only, so they are the same bytes on every machine, encodes them with 780 combinations of options and compares every MP3 withReference-Hashes-Signals-<platform>.csv, the hashes the publishedlameof that platform writes (bin\win-x64\lame.exewrites the same as unmodified LAME 4.0, see the reduced LAME). The tables of Windows and Linux have the same contents.-WavDirencodes WAV files of your own with yourlameand your ctLame and compares the outputs. On music,lameitself can write a few files differently on Windows and Linux, since the C math libraries (UCRT, glibc) round some functions differently in the last bit (2 of 1020 cases in our corpus, at-V 6.5and-V 9with-q 7); ctLame follows thelameof its platform, so compare programs of the same platform.-Matrix Smoke,QuickorFullchoose how many files and options;-ExtraArgs '--threads 1'or'--sequential'check the other execution paths,-Repeat 3output that depends on thread timing.Compare-LameCli.ps1(Windows only) runs both programs with 1012 command lines, including invalid ones, and compares exit codes, console output and the MP3 files.
Every check must end with all cases identical. 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).
All benchmarks run without ReplayGain (--noreplaygain), for both programs. Both run the
analysis by default, as unmodified LAME 4.0 does; the numbers below were measured without it, and
benchmark\ passes --noreplaygain to both (-ReplayGain measures them with it). With ReplayGain,
ctLame takes about 29 % longer on 16 threads (room for improvement).
Machine and method
| CPU | Intel Core i9-11900K (Rocket Lake), 8 cores, 16 threads |
| System | Windows 11 Pro, power plan High Performance, no other load |
| Programs | the binaries in bin\win-x64\; all numbers in this README are from Windows, there are no Linux measurements yet |
| Settings | -V 4 (LAME's default VBR quality), all other options at their defaults; no ReplayGain: neither measured program had it yet, and benchmark\ passes --noreplaygain to both |
| Timing | wall time of the whole process (start, reading the WAV file, encoding, writing the MP3), one process at a time; every run reads a fresh copy of the WAV file that is not in the file cache (cold); per file the programs take turns, three rounds, the median of each file counts, the totals are the sums of the medians |
| Check | every MP3 of ctLame compared byte for byte with lame.exe's |
| Run | 17 October 2026 with benchmark\run.cmd <folder> -Rounds 3, the script in this repository |
343 WAV files ripped from CDs (44.1 kHz, 16 bit, stereo), 45 h 5 min of music in all: 313 single tracks of 39 seconds to 9 minutes (21.7 hours) and 30 complete albums or discs saved as one file each, 17 to 79 minutes long (23.4 hours): blues rock and classical crossover.
| Program | Time | Real-time factor | vs. lame.exe |
|---|---|---|---|
lame.exe (reference) |
1195.2 s | 136× | 1.00 |
ctLame (all 16 threads) |
85.2 s | 1905× | 14.02 |
All 343 MP3 files of ctLame were bit-identical to lame.exe's.
On a single thread (ctLame --sequential, measured on 21 of the files, 3 h 9 min of music, one
round) ctLame took 50.5 s against 87.8 s for lame.exe, 1.74 times as fast, thanks to
restructured loops and SIMD paths (SSE4.1, AVX2) that compute the same operations in the same
order as the scalar code, so they too give identical bits.
Per file: short files gain less
| Files | Audio | lame.exe | ctLame | vs. lame.exe | per file: median (min to max) |
|---|---|---|---|---|---|
| 313 single tracks | 21.7 h | 571.3 s | 43.4 s | 13.16 | 13.30 (5.25 to 15.94) |
| 30 albums in one file | 23.4 h | 623.9 s | 41.8 s | 14.92 | 14.86 (13.51 to 16.75) |
The shorter the file, the more the start of the process and the filling and draining of the pipeline weigh: the lowest factor, 5.3, belongs to a 39-second intro, while every file of more than 16 minutes is encoded at least 13.5 times as fast.
The same 343 files, the same binaries and method (benchmark\run.cmd <folder> -Rounds 3):
| CPU | Cores / threads | lame.exe | ctLame | Real-time factor | vs. lame.exe |
|---|---|---|---|---|---|
| Intel Core i9-11900K (Rocket Lake, desktop) | 8 / 16 | 1195.2 s | 85.2 s | 1905× | 14.02 |
| Intel Xeon E3-1275 v6 (Kaby Lake, server, Windows Server 2016) | 4 / 8 | 1733.3 s | 217.1 s | 748× | 7.98 |
| Intel Core Ultra 7 155H (Meteor Lake, notebook) | 6 P + 8 E + 2 LP-E / 22 | 1531.8 s | 208.2 s | 780× | 7.36 |
Split by file length: the notebook slows down under sustained load
| CPU | single tracks | albums in one file |
|---|---|---|
| Core i9-11900K | 13.16 | 14.92 |
| Xeon E3-1275 v6 | 7.95 | 8.02 |
| Core Ultra 7 155H | 9.51 | 5.79 |
On the notebook ctLame keeps up on the single tracks but loses half its speed on the albums, which ran last; lame.exe, on one core, does not slow down there (see room for improvement).
Every encoding was bit-identical to LAME on every machine.
Parallelizing an MP3 encoder is easy if you may change its output: cut the audio into pieces and encode them independently. ctLame may not. Its output has to be exactly LAME's, and LAME is sequential by design:
- the psychoacoustic model carries state from frame to frame: attack detection, the switch between long and short blocks, pre-echo control, the adaptive hearing threshold;
- the bit reservoir lets a frame borrow bits that earlier frames left unused, so how a frame is coded depends on every frame before it;
- the MDCT filterbank needs the samples of the previous granule.
ctLame splits the work of each frame (1152 samples) into the parts that need this state and the parts that do not. The stateless parts (FFTs, spreading, MDCT, the search for scalefactors and quantization, Huffman coding) run for many frames at once on all cores; the stateful parts run as thin links in frame order. Each frame passes five stages, numbered as in the code:
flowchart LR
subgraph io ["I/O scheduler · 1 thread"]
R["Read<br/>overlapped ReadFile"]
W["Write<br/>overlapped WriteFile"]
end
subgraph cpu ["compute scheduler · one thread per logical processor"]
D["1 Dispatch<br/>in order"]
P["2 Prepare ×N<br/>psychoacoustics without state<br/>(the workers)"]
V["3 Resolve<br/>psychoacoustics with state<br/>in order"]
S["4 Search ×N<br/>MDCT, quantization search, Huffman<br/>(the same workers)"]
E["5 Encode<br/>bit reservoir, bit stream<br/>in order"]
end
R -- "PCM pipe" --> D
D -- "frame ring<br/>+ sample stream" --> P
P -- "frame ring" --> V
V -- "frame ring" --> S
S -- "frame ring" --> E
E -- "MP3 pipe" --> W
How the graph grew, step by step → from LAME's single loop to this graph, with the reason and the measured gain of each step.
Prepare and Search are not two groups of tasks: the same N worker tasks, one per compute thread, take whichever work is ready, searches first, since they belong to older frames and free slots sooner. Dispatch converts the WAV data into the sample stream, one float stream per channel in virtual memory; each frame in the ring points to its window there instead of copying it, and Encode releases the window when the frame is done.
The frames in flight live in a ring of four slots per compute thread (16 to 128), the size that measured best: fewer slots leave threads waiting for the ordered stages, more cost cache. Every node is a task on ThinkMeta.ConcurrentTasks: when a node waits for the frame before it, its task is suspended, and the thread runs other work meanwhile. Handing a frame from one thread to another costs about 0.4 µs, against about 200 µs of work per frame.
Every change was checked against LAME's output: SHA-256 of 1372 encodings of 343 files and of about 1000 encodings with other settings and input formats (mono, 32/48 kHz, 8/24/32 bit, float), plus a comparison of 1012 command lines with all their console output.
lame\ is LAME 4.0 (release RELEASE__4_0, SVN r6552) reduced to what ctLame implements:
MPEG-1 Layer III with the "new" VBR routine (-V, vbr_mtrh) from WAV files.
| Kept | Removed |
|---|---|
VBR new: -V 0..9.999, -q, the VBR presets |
CBR, ABR, VBR old, free format |
| MPEG-1: 32, 44.1 and 48 kHz | MPEG-2/2.5, resampling |
| WAV input: PCM 8/16/24/32 bit, 32-bit float, also WAVE_FORMAT_EXTENSIBLE | AIFF, raw PCM, MP3 input, stdin and stdout |
| Xing/LAME VBR header, ID3v1/ID3v2 tags and ReplayGain (removed at first, now back) | the frame analyzer, the ID3 options --id3v2-utf8, --id3v2-ucs2, --space-id3v1 and --pad-id3v2 |
| all options that tune VBR new: channel mode, bitrate limits, filters, ATH, noise shaping, short blocks, bit reservoir, scaling | the decoder, --nogap, the CPU/assembly dispatch (the SSE2 intrinsics path is always on) |
Size, output and build
The tree shrank from 319 to 70 files; libmp3lame from 26 301 to 19 838 lines, the frontend from
11 188 to 4 049 lines, lame.h from 1 357 to 971 lines (with ID3 and ReplayGain back; without them
it was 16 782, 3 375 and 800), with no compiler warning left (57 before, x64, warning level 4). The core of VBR new (psymodel.c, vbrquantize.c, newmdct.c, fft.c,
tables.c, reservoir.c) is nearly untouched.
Output: for the same options and a supported input file, the reduced lame.exe writes the same
bytes as unmodified LAME 4.0, ID3 tags and ReplayGain included. The one exception: unmodified LAME lowers
the sample rate at -V 6.5 and above (44.1/48 kHz) and at -V 8 and above (32 kHz); the reduced
LAME always keeps the input rate, and is then identical to unmodified LAME with
--resample <input rate>. The default is -V 4 instead of CBR 128 kbit/s.
ctLame.slnx builds it (Building); the result is build\Release\bin\lame.exe, built
with the settings of bin\win-x64\lame.exe; on Linux lame/CMakeLists.txt. Each published
program has its fastest build: ctLame.exe is in addition profile-guided optimized (PGO) and
built without buffer security checks, which makes it about 4 % faster; for lame.exe both made it
slower (by 2.6 %), so it keeps the plain build.
ctLame takes LAME's command line; ctLame --help and ctLame --longhelp list everything.
ctLame [options] <infile.wav> [outfile.mp3]
Options of ctLame only:
| Option | Meaning |
|---|---|
--threads n |
compute threads, 1 to 256; default: all logical processors |
--sequential |
the whole encoder as one task on one thread (the reference path) |
--simd level |
the highest SIMD level to use: scalar, sse4.1 or avx2; default: the highest the CPU supports. All levels give the same bits |
--features |
print the CPU features and the SIMD level in use |
--stats |
print the time of each stage and the busy and idle time of each task after encoding (also: environment variable CTLAME_PROFILE) |
LAME's options, all of which ctLame accepts
| Option | Meaning |
|---|---|
-V n |
VBR quality, 0 (best, largest) to 9.999 (smallest); default 4 |
-q n |
algorithm quality, 0 (best, slowest) to 9; default 3. -h = -q 2, -f = -q 7 |
--preset type |
medium, standard or extreme: the same as -V 4, -V 2, -V 0 |
-b n, -B n, -F |
minimum and maximum bitrate in kbit/s, enforce the minimum strictly |
-m mode |
j joint stereo (default), s simple stereo, f forced mid/side, d dual mono, m mono, l left, r right |
-a |
downmix stereo to mono |
--lowpass f, --highpass f, --lowpass-width f, --highpass-width f |
filters, in kHz |
--scale x, --scale-l x, --scale-r x, --gain dB, --swap-channel |
input scaling |
--ignorelength |
ignore the data length in the WAV header |
-Y, -Z [n], --athaa-sensitivity x, --r3mix |
psychoacoustic tuning |
-t, -T |
do not write / force the LAME tag |
-p, -c, -o, -e emp |
CRC, copyright and original flags; emphasis n, 5 or c |
--nores, --strictly-enforce-ISO, --buffer-constraint c |
bit reservoir and stream constraints; c is default, strict or maximum |
--quiet, --silent, --brief, --verbose, -S |
console output |
--priority n |
process priority, 0 (idle) to 4 (high); on Linux the nice value 10, 0 or -10 (raising it needs the rights to do so, otherwise the priority stays) |
--license, --version |
license and version |
ctLame writes ID3 tags and the ReplayGain of the input like unmodified LAME 4.0, with LAME's options;
the reduced LAME in lame\ has neither.
| Option | Meaning |
|---|---|
--tt, --ta, --tl, --ty, --tc, --tn, --tg |
title, artist, album, year, comment, track (n or n/total), genre (name or number) |
--tv id=value |
any ID3v2 frame, e.g. TPE2=Album Artist, TPOS=1/2, TXXX=description=value |
--ti file |
album art (JPEG, PNG or GIF) |
--add-id3v2, --id3v1-only, --id3v2-only |
which tags to write; by default the version 1 tag, and the version 2 tag when a text does not fit into version 1 or is Unicode |
--id3v2-utf16, --id3v2-latin1 |
the text encoding of the following options; UTF-16 is the default |
--pad-id3v2-size n, --ignore-tag-errors, --genre-list |
padding of the version 2 tag, go on after a bad tag value, list the genres |
--noreplaygain |
no ReplayGain analysis; --replaygain-fast is the default and does nothing else |
ctLame --help id3 lists the tag options; the reduced LAME has the same options and messages again.
The ID3v2 tag goes in front of the MP3 frames, the ID3v1 tag behind them. Tags and ReplayGain are
bit-identical to unmodified LAME 4.0 and to the reduced LAME, checked on Windows and Linux; for a
few corner cases of the tag code ctLame follows the fixes of LAME 4.1 instead (a
description that begins another one names a different frame, frame ids need four characters, short
language codes are padded, the padding is at most 128000 bytes, a tag too large for its size field is
left out).
The ReplayGain analysis (two IIR filters per channel over the whole file) runs in a task of its own per channel, beside the frame stages, and costs about 13 % of the time on one thread and about 29 % with 16 threads (room for improvement). Like LAME 4.0, ctLame writes only the track gain ("radio") into the LAME tag; players that read ReplayGain from the ID3v2 tag do not see it.
The parallel version adds a thin task layer (src\Tasks\) around an encoder that does not know
about tasks (src\Encoder\). Read it in this order:
src\App\Application.cpp,RunEncodeGraph: the setup of ThinkMeta.ConcurrentTasks, the same in every sample:Shared::TaskRuntime(..\Shared\src\Shared\TaskRuntime.h) makes the main thread a task withConvertThreadToTask, becauseEncodeGraph::Runwaits for the graph on it, starts the core's completion port for the overlapped file I/O (EnableAsyncIo) and an I/O scheduler with one thread for Read and Write.EncodeGraph::Initializeallocates the edges; aRunningScheduleris the compute scheduler with one thread per worker (--threads, else one per logical processor). The graph runs; on return the compute scheduler stops, the graph goes, andTaskRuntimeundoes its steps in reverse order. The rest ofsrc\App\is LAME's frontend: the command line (CommandLine.cpp), the WAV header (WavFile) and the messages.src\Tasks\EncodeGraph.h: the graph as a diagram, then its members, the edges and the nodes in the order the data flows.EncodeGraph::Run(EncodeGraph.cpp) creates the nodes, puts all of them into oneTaskGroup_s(one waiter: the caller), hands them to the schedulers withITaskScheduler::AddTask(Read and Write to the I/O scheduler; Encode and Resolve first and withHighpriority, then Dispatch and the workers to the compute scheduler) and waits for the group. The graph owns its edges and their events and destroys the nodes before the edges.- The edges:
BlockPipe(Read → Dispatch, the PCM pipe; Encode → Write, the MP3 pipe),PcmFeed(WAV bytes into the sample stream),SampleStream(src\Encoder\: one float stream per channel in reserved virtual memory, committed as it grows and decommitted behind the finished frames) andFrameRing(the frames in flight through the five stages). Their methods follow one vocabulary:Acquire…may wait and hands out the next piece of work,TryAcquire…does not wait,Complete…orCommit…hands the result on,Release…gives a block or a frame back. - The nodes (
src\Tasks\), each a plain loop that waits on an edge:WavReaderTask(Read),DispatchTask,WorkerTask(Prepare and Search),ResolveTask,EncodeTaskwithFrameEncoder(Encode) andMp3WriterTask(Write), and with ReplayGain oneGainTaskper channel, which Dispatch wakes and Encode waits for before the LAME tag.SequentialEncodeTaskis the one compute node of--sequential, with the same edges and the same encoder.ProfiledRunandWhileIdle(NodeProfile.h) only measure the nodes with--stats(or the environment variableCTLAME_PROFILE); read past them. - The encoder (
src\Encoder\):Encoder.hhas the four calls a frame goes through, one for each of the stages 2 to 5.PrepareandSearchareconst, so the compiler checks that they keep no state and may run for many frames at once;ResolveandEncodetake the frames in order.Stages.hsplits the stages further (2a to 5b),FrameData.hholds what belongs to one frame, the…StageStatestructures what a stage carries from frame to frame. The other files are the port of LAME; docs/porting.md maps LAME's functions to them.
The five stages of a frame, with the same numbers in FrameRing.h, Stages.h, Encoder.h and
the history of the graph:
| Stage | Frames | Node | Encoder call | Work (Stages.h) |
|---|---|---|---|---|
| 1 Dispatch | in order | DispatchTask |
the frame's window of the sample stream | |
| 2 Prepare | any order, N workers | WorkerTask |
Encoder::Prepare |
2a input, 2b psychoacoustic model without state, 2c polyphase filterbank |
| 3 Resolve | in order, High |
ResolveTask |
Encoder::Resolve |
3a psychoacoustic model with state, 3b filterbank carry |
| 4 Search | any order, N workers | WorkerTask |
Encoder::Search |
4a MDCT, 4b stereo decision, 4c quantization search and main data |
| 5 Encode | in order, High |
EncodeTask, FrameEncoder |
Encoder::Encode |
5a quantization with the bit reservoir, 5b bit stream |
"Encode" alone always means stage 5; EncodeGraph is the whole graph, and
Encoder::RunAllStages runs the four calls on one frame after the other (for the unit tests).
Why the frame ring is a class of its own and not the library's Semaphore_s: the stages need
more than a count (which frame, which kind of work, searches before preparations), and a counter
per stage plus an event costs one atomic operation per frame and stage. Three of its events have
exactly one waiter (Dispatch, Resolve and Encode) and use the cheaper AutoResetEvent_s; the
workers share one AutoResetEvent. FrameRing.h describes the protocol, and
src\Tests\FrameRingTests.cpp runs the ring under load with stand-in stages on a real scheduler.
BlockPipe, unlike the library's SpscVirtualMemoryPipe, holds back the producer when all blocks
are full, and its fill level sets the priority of the I/O tasks (IoPriority.h).
ctLame reports errors with LAME's messages and exit codes and does not try to recover:
- A read error while encoding ends the input there, like the end of the file: the frames read so far are encoded, the encoder is flushed and the LAME tag is written. ctLame prints "Error reading input file" and, like LAME, exits with 0.
- A write error ends the writing, but Write keeps taking the blocks from the MP3 pipe, so Encode never waits on a full pipe and the graph ends normally; then "Error writing mp3 output" and exit code 1. A LAME tag that is written only in part counts as a write error.
- An error of the encoder (LAME's "INTERNAL ERROR IN VBR NEW CODE", a frame too large for its buffer): Encode stops encoding but keeps taking the frames from the ring, so the stages before it finish; the message and the exit code are LAME's.
- Out of memory:
EncodeGraph::Initializechecks the allocations of the edges and the workers before the graph starts ("ctLame: out of memory", exit code 1), and a scheduler or the asynchronous I/O that cannot be created ends the program with "ctLame: cannot start the schedulers" (exit code 1). If the sample stream (or the one frame of--sequential) gets no memory while encoding, the input ends there and ctLame reportsmp3 internal error: error code=-3(exit code 1). Allocations outside these checks, such as the encoder itself at the start, end the process when they fail; the sample does not unwind its graph for that case.
ctLame is fast, but not at the end; the measurements point to these limits and next steps:
-
Notebooks under sustained load: on the Core Ultra 7 155H ctLame reaches 9.5× on the single tracks, but only 5.8× on the album-length files, which ran last, after about 35 minutes of full load. lame.exe on one core does not slow down on them (121× against 94× real time on the single tracks), so this is probably the notebook's sustained power or thermal limit, which cuts the clock of all cores when all of them are busy. A desktop CPU (14.9× on the albums) and the server CPU (8.0×) show no such drop. Whether fewer threads give more there is still to be measured.
-
Hybrid CPUs: apart from that, performance and efficiency cores share the work well: on the 155H all 22 threads are within 2 % of the best thread count. AVX2 brings nothing over SSE4.1 there, probably because the efficiency cores execute 256-bit instructions in two halves. A scheduler that knows the core types is part of the research project.
-
The serial links: at 16 threads ctLame finishes a frame every 13 µs. The in-order part of the psychoacoustics (stage 3, Resolve: attack detection, block switching, pre-echo control) needed about as much per frame before its pre-echo control was vectorized, so on this CPU it runs close to its limit. A split of Resolve into two short serial parts (about 5 µs per frame together) with a parallel part between them was tried and was bit-identical, but gave nothing on 8 cores, where the workers are fully busy; it is not in this code and waits for CPUs with more cores. The bit stream at the end (stage 5, Encode) takes about 4 µs per frame, since the Huffman coding moved into the parallel search.
-
Turbo clock and hyperthreading: the same work costs more time the more threads run: about 16 % more with 4 threads, 25 % with 8 (lower turbo clock, shared cache) and 50 % with 16, when two threads share a core. That is why 16 threads on 8 cores are 8 times as fast as one thread, not 16 times. With four cores (Xeon E3) ctLame reaches 8.0×, evenly for short and long files; there speed comes only from less work per frame.
-
Short files: a 39-second file is encoded only 5 times as fast; starting the process and filling and draining the pipeline take a larger share there.
-
Less work per frame: most of the time goes into the search for scalefactors and quantization, the psychoacoustic preparation and the MDCT. Long sums stay scalar because their order is fixed by bit identity. Tried and dropped: AVX2 gather (half as fast as scalar loads on Rocket Lake), AVX-512 lookup tables (would cover only 3 to 7 % more blocks) and a mode without bit identity (fused multiply-add): 2 % faster on one thread, nothing on 16.
-
ReplayGain: its analysis is a serial chain of two IIR filters per channel. Each channel runs in a task of its own, the input part of the first filter with SIMD, the feedback of both filters in one loop; still it costs about 29 % with 16 threads, because it is CPU work on a CPU the encoder already keeps busy (with empty Gain tasks the time is that of
--noreplaygain).--noreplaygainsaves it. Like LAME 4.0, ctLame writes only the track gain, into the LAME tag; LAME 4.1 adds--replaygain-id3v2(the values as ID3v2 frames, which most players read), and an album gain would need all files of an album. -
More for everyday use: tags taken from the WAV file (its LIST/INFO or
id3chunk), input from a pipe (programs like foobar2000 pass the audio that way), and input above 48 kHz, which would need a resampler. -
Linux: no measurements yet, and the benchmark script and
Compare-LameCli.ps1are Windows only; a Linux benchmark script would come first. The PGO gain of the Linux build (clang) is not 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.
Every step keeps the output bit-identical to LAME.
ctLame is a showcase for ThinkMeta.ConcurrentTasks, a task scheduler for Windows and Linux (x64) by ThinkMeta Software GmbH; ctLame runs on both. Tasks run on fibers: a task can wait for a frame, an event or an asynchronous file read without blocking its thread. A handful of threads keeps every core busy while thousands of tasks wait, and code that waits stays as simple as sequential code.
- Small and fast core: the scheduler is a library of about 30 KB that depends on nothing but the operating system: a DLL on Windows (no C runtime), a shared library on Linux (the C library only). A task switch is a fiber switch; handing work to another thread costs well under a microsecond.
- Building blocks on top: tasks with priorities, events, semaphores and other synchronization primitives for tasks, concurrent collections, pipes in virtual memory, asynchronous file and socket I/O over I/O completion ports (Windows) and io_uring (Linux).
- Deterministic where it matters: ctLame shows that a pipeline with ordered and parallel stages can keep a strictly sequential result while using every core.
The library itself (headers, modules, core DLL and its documentation) is in
ThinkMeta.ConcurrentTasks.API,
included in this repository as the submodule
ThinkMeta.ConcurrentTasks.API\.
ctLame's ordered stages wait in the middle of their work: the dispatcher for a free slot in the ring of frames, resolve for the psychoacoustic state of the frame before, the bit reservoir for the bits that frame left over. On ConcurrentTasks such a wait suspends only the task, its thread meanwhile runs the parallel search of other frames, and each stage stays a plain loop over the frames in order. With a thread pool every one of these waits would either block a worker or have to be cut into callbacks that save their state and are queued again once the frame before is done, with a new frame about every 13 µs at 16 threads.
ctLame is a first look at ThinkMeta.ConcurrentTasks. From January 2027 to December 2029 the framework is being developed further in a research and development project that Germany's Bescheinigungsstelle Forschungszulage (BSFZ) has certified as research and development under the research allowance (Forschungszulage). Today it runs on Windows and Linux on x64; the project takes it further:
| Where | What |
|---|---|
| Operating systems | macOS besides Windows and Linux |
| Desktop and server CPUs | hybrid x64 processors with performance and efficiency cores (Intel Alder, Raptor and Arrow Lake, AMD Zen 4 and 5) |
| Game consoles | Xbox (GDK), PlayStation 5 and Nintendo Switch, on top of their own fiber APIs |
| Embedded CPUs and MCUs | ARM Cortex-A, -M and -R, RISC-V, Xtensa (ESP32) and PowerPC (automotive), on bare metal and next to RTOSes such as FreeRTOS and Zephyr |
The same task model on every platform: code that waits stays as simple as sequential code, and fine-grained work (steps of a few microseconds, as in ctLame) is spread over every core. ctOpus, ctDeflate and ctJpeg in the same repository apply it to further reference encoders.
Building a game engine, an industrial controller or another product that needs every core? Want to run your own benchmarks or work with us? www.thinkmeta.com
lame\andlameinbin\: LAME, GNU Library General Public License version 2 or later (COPYING). Copyright (c) 1999-2011 The LAME Project and the authors named in the sources.src\andctLameinbin\win-x64\andbin/linux-x64/: a port of LAME, therefore also under the GNU LGPL version 2 or later (COPYING,src\ctLame-LICENSE.md). Copyright (c) 2026 ThinkMeta Software GmbH for the port, LAME's copyrights for the original.- The scheduler core (
ThinkMeta.ConcurrentTasks.Core.dll,libThinkMeta.ConcurrentTasks.Core.so.2inbin\): proprietary, Copyright (c) 2026 ThinkMeta Software GmbH; terms of use inLICENSE-Core.txt. ctLame loads it at run time. The terms cover the core only: they permit its private and non-commercial use, commercial use of the core requires a license from ThinkMeta Software GmbH; they do not limit the rights the LGPL grants for ctLame. The Linux programs contain the GNU C++ library and libgcc, linked in under the GCC Runtime Library Exception. benchmark\,verify\,ThinkMeta.SolutionFiles\and the build files: MIT license (LICENSE.txt), Copyright (c) 2026 ThinkMeta Software GmbH (.clang-formatthere is a copy from the submodule).
Benchmarks are explicitly permitted: anyone may run benchmark tests of any kind with the
programs in bin\, 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).