vorbis_decoder_ffi
Ogg Vorbis audio decoder for Flutter, wrapping the public-domain
stb_vorbis
C decoder via dart:ffi. Decodes a complete in-memory Ogg Vorbis stream to
interleaved signed 16-bit PCM — no file I/O, no platform channels.
| Platform | Build |
|---|---|
| iOS 13+ | Swift Package Manager (CocoaPods fallback) |
| Android (minSdk 24) | CMake / NDK |
| macOS 10.15+ | Swift Package Manager (CocoaPods fallback) |
Why
Dart has no Ogg Vorbis audio decoder (pub.flutter-io.cn "vorbis" results are tag readers or encoders). This package fills that gap for use cases like:
- SF3 SoundFonts — SF3 is SF2 with the sample chunk compressed as Ogg Vorbis. Decode each compressed sample to PCM at load time and feed your synth the same data an SF2 would contain.
- Vorbis sound effects / music loaded from assets or network.
- Converting downloaded audio for playback engines that want raw PCM.
Usage
import 'package:vorbis_decoder_ffi/vorbis_decoder_ffi.dart';
final VorbisDecoded decoded = decodeOgg(oggBytes); // Uint8List in
decoded.pcm; // Int16List, interleaved
decoded.channels; // 1, 2, ...
decoded.sampleRate; // Hz
decoded.samplesPerChannel; // frames
decoded.duration; // Duration
// On the UI isolate, prefer the async variant for large inputs —
// decoding is CPU-bound:
final decoded = await decodeOggAsync(oggBytes);
Invalid input throws VorbisDecodeException carrying the underlying
STBVorbisError code and a readable message.
Zero-copy decoding into your own buffer
decodeOgg hands you a Dart-owned Int16List, which costs one native →
Dart copy. When you manage native memory yourself (e.g. assembling one
large sample bank for a synth), probe first, then decode straight into
your buffer:
import 'dart:ffi';
import 'package:ffi/ffi.dart';
final VorbisInfo info = probeOgg(oggBytes); // header scan only, no PCM
final Pointer<Int16> pcm = malloc<Int16>(info.totalPcmShorts);
try {
final VorbisInfo decoded = decodeOggInto(oggBytes, pcm, info.totalPcmShorts);
// pcm now holds decoded.samplesPerChannel * decoded.channels int16s.
} finally {
malloc.free(pcm); // caller owns the buffer
}
The buffer stays owned by the caller; nothing is allocated or freed on its
behalf. If the stream doesn't fit, decodeOggInto throws
VorbisDecodeException with code VorbisDecodeException.outputTooSmall
(the native decoder never writes past the given capacity).
Running tests on a host machine
FFI needs a native library; Flutter only bundles it for app builds. For
flutter test on macOS/Linux, build the host library once and the tests
pick it up automatically (the suite builds it on demand too):
./tool/build_host_lib.sh
flutter test
You can also point the loader at any library explicitly, before the first decode call:
vorbisDecoderLibraryOverride = '/path/to/libvorbis_decoder_ffi.dylib';
or via the VORBIS_DECODER_FFI_LIBRARY environment variable. In widget
tests of an app that merely imports this package, no library is needed
unless a decode actually runs.
Fixtures
test/fixtures/*.ogg are short generated signals; each has a .raw
reference (the same file decoded by ffmpeg to s16le). Tests assert RMS
closeness rather than bit-equality — Vorbis synthesis is floating-point, so
two correct decoders differ by rounding, not by content. Regenerate with
tool/generate_fixtures.sh (requires ffmpeg + vorbis-tools).
License
MIT. Vendored stb_vorbis is public domain (or MIT, at your option) — see
LICENSE.
Libraries
- vorbis_decoder_ffi
- Ogg Vorbis audio decoder for Flutter, backed by
stb_vorbisover FFI.