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_vorbis over FFI.