vorbis_decoder
A pure Dart package that decodes in-memory Ogg Vorbis data to interleaved
Float32List PCM. It is ported from
NVorbis, and the decoder core under
lib/ does not depend on Flutter, FFI, or dart:io.
- Compiles for the Dart VM, JavaScript, and Wasm
- Supports mono/stereo, Vorbis Floor 1, Residue 0/1/2, and Mapping 0
- Validates Ogg CRCs, lacing, packet continuation, serials, granules, and EOS
- Applies exact final-granule trimming
- Produces interleaved IEEE-754
Float32ListPCM by default - Provides an explicit
Float32ListtoInt16Listconversion API - Defends against malformed inputs and malicious allocation requests
The current recommendation is: suitable as a fallback when FFI is not available. Accuracy and the real SF3 application path have been verified, but this implementation is slower and uses more memory than FFI. Floor 0 and several Ogg features are also not supported yet. Replacing an application's production FFI decoder with this implementation is not currently recommended.
The accuracy, performance, memory, and application-validation evidence is summarized below.
Requirements
- Dart SDK
^3.3.0 - No runtime dependencies
Install the package from pub.flutter-io.cn:
dart pub add vorbis_decoder
Quick start
import 'dart:io';
import 'package:vorbis_decoder/vorbis_decoder.dart';
void main() {
// File access belongs to the caller. The decoder's lib/ does not use dart:io.
final oggBytes = File('sample.ogg').readAsBytesSync();
final decoded = decodeOgg(oggBytes);
print('channels: ${decoded.channels}');
print('sample rate: ${decoded.sampleRate}');
print('frames: ${decoded.frames}');
print('interleaved samples: ${decoded.pcm.length}');
final pcm16 = float32ToInt16(decoded.pcm);
print('int16 samples: ${pcm16.length}');
}
In browsers or Wasm applications, pass a Uint8List obtained from the
network, an asset, drag-and-drop, or another source directly to probeOgg or
decodeOgg.
Public API
probeOgg
Validates the Ogg pages and Vorbis headers and returns stream information without synthesizing PCM.
final info = probeOgg(oggBytes);
print(info.channels);
print(info.sampleRate);
print(info.frames); // Frames per channel.
print(info.totalSamples); // frames * channels
decodeOgg
Decodes the complete stream in one call.
final result = decodeOgg(oggBytes);
final Float32List pcm = result.pcm;
assert(pcm.length == result.frames * result.channels);
PCM is frame-major and interleaved:
mono: M0, M1, M2, ...
stereo: L0, R0, L1, R1, L2, R2, ...
float32ToInt16
Converts float PCM to signed 16-bit PCM. This is an explicit public API rather than the decoder's default storage format.
final Int16List pcm16 = float32ToInt16(result.pcm);
Conversion contract:
- Finite inputs are clamped to
[-1.0, +1.0]. - Values are rounded to the nearest integer, with exact halves rounded away from zero.
-1.0 → -32768+1.0 → +32767NaN → 0-Infinity → -32768+Infinity → +32767- Endpoints are handled before multiplication to prevent overflow.
Error handling
Malformed or unsupported streams throw VorbisDecoderException. When
available, the exception includes the relevant Ogg byte offset.
try {
final decoded = decodeOgg(oggBytes);
usePcm(decoded.pcm);
} on VorbisDecoderException catch (error) {
print(error.message);
print(error.offset);
}
The decoder checks PCM sample-count arithmetic for overflow and unsafe allocations. The one-shot API limits output to 64 Mi-samples.
Supported scope
| Feature | Status |
|---|---|
| One logical Ogg Vorbis stream | Supported |
| Vorbis Floor 1 | Supported |
| Residue 0, 1, and 2 | Supported |
| Mapping 0 and channel coupling | Supported |
| Mono and stereo | Verified |
| 8/22.05/44.1/48 kHz | Verified |
| JavaScript compilation | Supported |
| Wasm compilation | Supported |
| Vorbis Floor 0 | Unsupported; rejected explicitly |
| Chained or multiplexed Ogg | Unsupported |
| Seeking | Unsupported |
| Streaming input or incremental PCM | Unsupported |
Accuracy validation
Eleven pinned fixtures are compared against ffmpeg Float32 reference PCM. The comparator does not use trimming, padding, lag search, or length tolerances.
The validation checks:
- Channel count and sample rate
- Exact frame count and interleaved PCM length
- Finite values for every sample
- Overall and per-channel RMS error
- Maximum absolute error
- Trimming at both the beginning and end
Results:
| Metric | Result |
|---|---|
| Fixtures | 11/11 PASS |
| Exact frame/shape comparison with FFI | 11/11 PASS |
| Worst RMS error | 1.379e-7 |
| Worst maximum absolute error | 3.716e-6 |
Per-fixture thresholds and reference metadata are recorded in
test/fixtures/manifest.json.
Performance
The following values are p50 measurements from an AOT executable on macOS 26.5.1 arm64 with Dart 3.12.2.
| Input | Pure Dart Float32 | FFI end-to-end | Pure real-time factor |
|---|---|---|---|
| 0.5-second mono | 2.266 ms | 0.151 ms | 220.65× |
| 0.6-second stereo | 11.548 ms | 0.489 ms | 51.96× |
| 3-second music | 54.967 ms | 2.755 ms | 54.58× |
| 10-second stereo | 222.111 ms | 6.450 ms | 45.02× |
| Real SF3 sample, 21.1 seconds | 175.152 ms | 5.382 ms | 120.63× |
| Real SF3 eight-sample set, 191.9 seconds | 1.254 s | 45.420 ms | 153.03× |
The pure Dart decoder is comfortably faster than real time for representative inputs, but is approximately 15–35 times slower than FFI.
From a repository checkout, run the published-FFI comparison test with:
./tool/test_ffi_comparison.sh
To reproduce the benchmark:
./tool/benchmark_ffi_comparison.sh
Both scripts use the isolated package under dev/ffi_compare, resolve
vorbis_decoder_ffi from pub.flutter-io.cn, and compile its published C source into a
host library. The root vorbis_decoder package therefore remains independent
of Flutter, and dev/ is excluded from the published archive.
Memory
The following results use the 10-second stereo fixture. Each case runs in a fresh VM process.
| Path | Retained PCM | RSS increase | Estimated transient memory excluding input and result |
|---|---|---|---|
| Pure Dart → Float32 | 3,840,000 B | 12,173,312 B | 8,293,775 B |
| Pure Dart → Float32 → Int16 | 5,760,000 B | 14,827,520 B | 9,027,983 B |
FFI decodeOgg |
1,920,000 B | 1,654,784 B | Estimated as 0 due to allocator reuse |
FFI probe + decodeOggInto |
1,920,000 B | 1,228,800 B | Estimated as 0 due to allocator reuse |
The real SF3 lazy-loading path increased host RSS by 42,254,336 bytes. These are host-side estimates; allocator reuse limits the precision of transient memory estimates.
Real SF3 application validation
The decoder was validated by host-side application tests against the real SF3
loading and lazy sample-decoding paths under 03app. This does not represent
validation on a physical mobile device.
The following checks pass with the real bundled
salamander-light-v3.sf3:
- Preset and sample-header loading
- Non-silent
noteOnoutput - Volume or layer changes at different velocities
- Real lazy sample probing, decoding, and cache residency
- Legato, staccato,
noteOff, andstopAllcontracts - No remaining timer or hanging voice after disposal
- Existing 150 MiB eager PCM budget
The production backend remains FFI.
Malformed input and fuzz validation
Tests verify decoder exceptions for:
- Empty input, non-Ogg bytes, and truncated headers, pages, or packets
- Invalid capture patterns, Ogg versions, CRCs, lacing, or flags
- Continued-packet, serial, and sequence mismatches
- Missing EOS or data after EOS
- Backward granules, unsafe integer ranges, and malicious allocations
- Invalid identification versions, channels, sample rates, or block sizes
- Corrupt setup headers, codebooks, and audio packets
- Sample-count and buffer-size arithmetic overflow
A deterministic 128-case mutation suite uses seed 0x564f5242. No infinite
loop, VM crash, or leaked RangeError was observed.
Verification commands
dart analyze
dart test --reporter expanded
dart compile js tool/web_smoke.dart -o /tmp/vorbis_web_smoke.js
dart compile wasm tool/web_smoke.dart -o /tmp/vorbis_web_smoke.wasm
dart run tool/fixture_report.dart
Regenerating the fixture corpus additionally requires ffmpeg, ffprobe, oggenc, and a C compiler:
dart run tool/generate_fixtures.dart
Normal tests read only the committed Ogg files and ffmpeg references. They do not invoke external executables or access the network.
Project layout
lib/
vorbis_decoder.dart
src/
decoder.dart
ogg/
crc32.dart
packet_reader.dart
vorbis/
bit_reader.dart
core.dart
test/
decoder_fixture_test.dart
malformed_test.dart
probe_test.dart
fixtures/
tool/
fixture_report.dart
compare_pcm.dart
generate_fixtures.dart
web_smoke.dart
Recommendation
The current implementation is a good fit for:
- Web or Wasm environments where FFI is unavailable
- Environments where shipping a native library is undesirable
- Asynchronous lazy SF3 sample decoding as a fallback
- Offline decoding that requires exact frame counts
The existing FFI backend remains a better fit when:
- Selecting the default production backend for a mobile application
- Decode latency and peak memory are important
- Arbitrary Vorbis input may contain Floor 0 or chained Ogg streams
License and fixture attribution
This package is distributed under the MIT License. The NVorbis
reference implementation is also MIT-licensed. The recorded-piano
fixture source is derived from Alexander Holm's Salamander Grand Piano v3
under CC BY 3.0. Complete source and license information is recorded in
test/fixtures/FIXTURE-LICENSES.md.
Libraries
- vorbis_decoder
- Pure Dart Ogg Vorbis audio decoding.