You have an .ogg file and you need the raw samples. In Dart there is no decoder in the standard library, so you do the normal thing: you shell out to ffmpeg, read its stdout, and get your PCM. It works, it is one line, and ffmpeg is better at codecs than anything you are going to write.
I wanted to know what that costs, so I measured it against decoding in the same process. The interesting number turned out not to be the one I went looking for.
The measurement
Four clips of a 440 Hz sine, 44.1 kHz stereo, Vorbis in Ogg, at 1, 5, 15 and 30 seconds. Two ways to turn each one into interleaved 16-bit PCM:
-
In process.
decodeOgg(bytes)from my audio_decode package, which compiles stb_vorbis from source through a Dart build hook and calls it over FFI. -
Subprocess.
ffmpeg -i clip.ogg -f s16le -acodec pcm_s16le -, run withProcess.runSync, reading the PCM off stdout. ffmpeg 8.1.1.
Nine runs each, median reported, Apple M-series laptop. The bench checks that both paths produce the same amount of audio before it trusts either timing, which matters more than it sounds like and which I will come back to.
| clip | in process | ffmpeg | ratio |
|---|---|---|---|
| 1 s | 0.594 ms | 25.221 ms | 42x |
| 5 s | 2.108 ms | 27.406 ms | 13x |
| 15 s | 6.294 ms | 32.746 ms | 5x |
| 30 s | 12.923 ms | 40.452 ms | 3x |
Run to run these move by a few percent, and more than that on the sub-millisecond cell: a second run, from a fresh clone of the repo, gave 0.622 ms and 24.727 ms in the first row. Nothing below depends on the third decimal.
A 42x ratio at one second is the sort of number that ends up in a README, and it would be a dishonest one. Look at what the ffmpeg column actually does: thirty times more audio, and the time goes up by 15 ms. The clip length is barely moving that number. Something else is most of it.
The floor
The way to find out is to give ffmpeg nothing to do. I encoded a 0.05-second clip, 4 KB, and decoded it the same way:
Decoding a 0.05 s clip through ffmpeg still takes 24.793 ms, so that
is roughly what the process itself costs, before any audio is decoded.
That is the answer. About 24.8 ms of the 25.2 ms at one second is not decoding. It is fork, exec, the dynamic linker resolving ffmpeg's dependencies, ffmpeg parsing its arguments, probing the input format, setting up filters, and tearing all of that down again.
Subtract it and the two decoders look like what they are, which is two competent implementations of the same codec:
| clip | in process | ffmpeg, decoding only |
|---|---|---|
| 1 s | 0.594 ms | 0.428 ms |
| 5 s | 2.108 ms | 2.613 ms |
| 15 s | 6.294 ms | 7.953 ms |
| 30 s | 12.923 ms | 15.659 ms |
ffmpeg is faster than me at one second and I am faster at thirty. Nobody should switch decoders over that. The 24.8 ms is the whole story, and it has nothing to do with audio.
What it actually costs you
Because the floor is fixed per invocation, the arithmetic is per file, not per second of audio:
- One 30-minute podcast: ffmpeg spends ~25 ms starting up and around a second decoding. The startup is 2-3% of the job. Shell out; it is fine.
- A thousand short voice clips: 1000 × 24.8 ms is about 25 seconds, spent entirely on process startup, before counting a single sample. Batch them into one ffmpeg call and that goes away, but the moment the files arrive one at a time, or need different decode options, you are back to paying it per file. In process, it never exists.
- A Flutter app decoding a clip in response to a tap: 25 ms is a frame and a half at 60 fps, on top of whatever the decode costs, and you cannot amortise it because there is one clip.
That last one is the case where it stops being an optimisation and becomes a design constraint. It is also the case where shelling out is often not available at all: on iOS you cannot spawn a process, so there the subprocess column is not a slower option, it is not an option.
The honest summary is narrow. If you decode occasionally, or in a batch job where a few seconds of overhead disappears, run ffmpeg and enjoy its enormous format coverage. If you decode many files, or decode on a latency budget, or ship somewhere without a shell, the per-file floor is the reason to pull the decoder in process.
The check that made the numbers mean anything
Before the bench times anything, it decodes the clip both ways and checks that each produced the same amount of audio. This felt like belt-and-braces when I wrote it. It was not.
A first, stricter version of the check compared the raw sample counts and threw:
sample count differs for 1s: 88200 vs 87944
128 frames short, 256 samples across the two stereo channels, on the one-second clip only. Two decoders disagreeing about how much audio is in a file is exactly the kind of thing that quietly makes a benchmark meaningless, so I went and looked instead of loosening the check.
The container settles it. ffprobe reports duration_ts=44100 for that clip, which is the frame count the Ogg stream declares. My decoder returns 44100 frames. ffmpeg's raw PCM pipe delivers 43972, stopping 128 frames early, and only at the shortest clip:
1s: declared 44100 audio_decode 44100 ffmpeg pipe 43972
5s: declared 220500 audio_decode 220500 ffmpeg pipe 220500
15s: declared 661500 audio_decode 661500 ffmpeg pipe 661500
30s: declared 1323000 audio_decode 1323000 ffmpeg pipe 1323000
A tail-trimming edge case at very short durations. 128 frames is 2.9 ms of audio, so for most uses it does not matter. But it does mean the check has to be about how much audio each side produced, counted in frames, not about the samples matching byte for byte. The samples do not match byte for byte, and that is the second thing worth knowing: stb_vorbis and ffmpeg's Vorbis decoder are two different decoders, so about half the samples differ by one least-significant bit. Both are correct; floating-point Vorbis synthesis simply does not have a single right answer to the last bit. A check that demanded identical samples would fail on every clip and tell you nothing.
So the check counts frames: it reports the 128-frame gap rather than hiding it, and fails only if the two decoders ever disagree by more than about a thousand frames, a codec block's worth, which would mean one of them is genuinely dropping audio. A benchmark that does not check that both sides did the same work is measuring something, and you do not know what.
Run it yourself
The timings in this post come from a script in the package, not from my notes:
git clone https://github.com/Yusufihsangorgel/audio_decode
cd audio_decode
dart run bench/vs_ffmpeg.dart
It needs ffmpeg on your PATH and a C toolchain for the build hook, generates its own clips, and prints the table above plus the measured floor for your machine. If your floor is very different from 24.8 ms I would genuinely like to know: process startup varies a lot by OS and by how ffmpeg was built, and the whole argument in this post is downstream of that one number.
None of this is specific to audio. Shell out to a tool in a hot path and you pay for the process, not the tool, at the same price whether you asked it to do a little work or a lot.

Top comments (0)