NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev
Decode MP3 and Ogg Vorbis to raw PCM in Dart, over FFI. Compiles stb_vorbis and minimp3 from source: self-contained, pure-Dart friendly, no platform plugins or prebuilt binaries.
Last release 15 days ago
23 Sep 2026
Ships fairly regularly
a new release about every 2 weeks
Nearly every release is documented
notes for 29 of 29 stable releases
Nothing withdrawn
no release was ever pulled
2 months old
29 releases · first in 2026
One column per month.
The build hook returns early when a build does not request code assets. It read the target OS for the decoder build first, and that setting exists onl
Update code_assets to 2.0.0 for native build hooks.
code_assets to 2.0.0 for native build hooks.New example/info_without_decoding.dart, the first runnable use of audioInfo in this package. The README already timed it against decodeAudio; this mea
example/info_without_decoding.dart, the first runnable use of
audioInfo in this package. The README already timed it against
decodeAudio; this measures the other half, which is memory: the fixtures
decode to 33-52x their size on disk, and a decoded album is most of a
gigabyte to answer a question the header already holds.The section on why to reach for this rather than the alternative now sits directly under the recording, instead of below a long explanation. Someone d
The README opens with a recording of the package running, rendered from a real execution rather than drawn: the command is run, its exit code checked,
tools/term-trailer.sh in
the portfolio repository makes it, so it can be regenerated when the numbers
move.The README now answers, in its first screen, why to reach for this rather than the zero-dependency route or the package that already owns the category
Documents how to ship a standalone binary. dart compile exe refuses outright on a package with a build hook; dart build cli runs the hook and writes t
dart compile exe refuses outright
on a package with a build hook; dart build cli runs the hook and writes the
executable and its library into a bundle/ directory. The binary resolves the
library through a relative ../lib, so a copy of it on its own fails at the
first call, which is why the whole folder has to ship. Both halves were run to
produce the output quoted in the README.New example, example/speech_input.dart: the conversion in front of a speech model, run end to end. It decodes the stereo fixture, writes the 16 kHz mo
example/speech_input.dart: the conversion in front of a speech
model, run end to end. It decodes the stereo fixture, writes the 16 kHz mono
WAV with toSpeechPcm, then takes a 12 kHz tone down to 16 kHz twice, once
through resample() and once by plain decimation, and reports the energy
left at the 4 kHz alias by each path: 0.1% against 88.3% on an Apple
M-series laptop. The cost of skipping the filter stops being a claim once
the unfiltered number is on your own screen.whisper_ggml 2.6.0 bundles FFmpeg on Android, iOS and macOS but converts
nothing on Windows or Linux without an ffmpeg on PATH, and its live
entry point takes 16 kHz mono PCM16 on every platform; vosk_flutter 0.3.48
never converts. The gap is narrower than "everyone needs this", and real
where it exists.git status. The 1.2.0 archive
itself shipped one, a stray 172 KiB sine_44100_stereo_1s.ogg.wav at the
package root produced exactly that way, and this release removes it.decodeAudio and audioInfo doc comments still said they throw for
anything that is "neither Ogg Vorbis nor MP3". WAV has been decoded since
1.1.0; the docs now name all three formats. Two dartdoc ranges written as
[-1.0, 1.0] rendered as broken reference links and are backticked now.dart compile exe currently produces a binary
that cannot resolve the native library: the compile exits 0 and the binary
dies on its first decode, on Dart 3.11.0. dart run and dart test are
unaffected.Add `toSpeechPcm`, `resample` and `toMono`. Whisper, wav2vec 2.0 and Vosk all want 16 kHz mono 16-bit PCM, and a decoded file is almost never that. Ev
toSpeechPcm, resample and toMono. Whisper, wav2vec 2.0 and Vosk
all want 16 kHz mono 16-bit PCM, and a decoded file is almost never that.
Every caller feeding a speech model was writing that conversion by hand,
against a package that was already holding the samples. toSpeechPcm(pcm) is
the one line between the two.toMono averages channels instead of keeping one, and rounds away from zero
so quiet material does not drift toward silence.The example runs without an argument. It used to print a usage line and exit 64 unless you had an audio file to hand, which is the first thing anyone
test/fixtures/sine_44100_stereo_1s.ogg, which ships in the archive,
and prints the format, rate, channels, frames, duration and ASCII waveform
before writing the WAV. The output says the tone is a steady sine so the flat
waveform reads as correct rather than broken. A path argument behaves as
before, and the WAV is now written to the file's basename rather than beside
the input.Read WAV, so the package can open what `encodeWav` writes. encodeWav has been here since the start, but detectFormat did not know the RIFF signature:
Read WAV, so the package can open what encodeWav writes. encodeWav
has been here since the start, but detectFormat did not know the RIFF
signature: feed its own output back to decodeAudio and it threw
"unrecognized audio format". Anyone walking a directory of mixed audio hit
the same wall on the one format that needs no decoder at all.
decodeWav and wavInfo are pure Dart — a WAV file is a header and the
samples — and handle integer PCM at 8, 16, 24 and 32 bits plus IEEE float at
32 and 64, converting each to the signed 16-bit samples PcmAudio carries.
8-bit is unsigned and centred on 128, unlike every other width, and float is
clamped so an overshooting recording or a NaN from a broken encoder cannot
wrap into a loud opposite-sign sample. detectFormat returns
AudioFormat.wav, and decodeAudio and audioInfo dispatch to them.
wavInfo reads the frame count from the size of the data chunk rather than
by counting, so it costs the same on a four-second clip and a four-hour one.
A compressed payload in a RIFF wrapper — ADPCM, µ-law, an MP3 inside a WAV — is named in the error rather than decoded as noise. The big-endian RIFX variant is not read.
Declare the SDK this package can actually resolve on. The constraint read ^3.9.0, but the hooks dependency that runs the native build requires >=3.10.
^3.9.0, but the hooks dependency that runs the native build requires
>=3.10.0. On Dart 3.9 the package looked supported and then failed to
resolve, with an error naming hooks rather than anything the reader had
asked for. The pubspec and the README now both say 3.10.1.0.1: link libm on Android, and stop returning success with no audio
1.0.1: link libm on Android, and stop returning success with no audio
Two bugs found by exercising the package against its own pub.dev description on a real device and a real file sweep, rather than against its own tests.
flutter build apk succeeded for all three ABIs, and then on device:
dlopen failed: cannot locate symbol "log" referenced by libaudio_decode.so
stb_vorbis and minimp3 call log, pow, sin, exp and ldexp. Android
keeps those in a separate libm, and its linker will not resolve a symbol from
a library that is not in DT_NEEDED. The build hook never linked it, so the
shipped .so listed only libdl.so and libc.so.
Every other target hid the omission: macOS and iOS get math from libSystem,
and glibc 2.34 folded libm into libc. Android was the one platform where it
was fatal, and the one platform a Dart test/ directory cannot reach.
hook/build.dart now links m on Android and Linux. Verified by reading
DT_NEEDED out of the built .so: it was [libdl.so, libc.so] and is now
[libm.so, libdl.so, libc.so].
If you shipped 1.0.0 to Android, it did not work. Sorry.
Truncating an Ogg part way through its first audio page returned a PcmAudio
with frameCount == 0 and threw nothing. Any caller validating an upload by
catching an exception accepted a file that produced no audio at all. A decode
that yields no frames now throws AudioDecodeException.
The README's claim that truncated Ogg files "fail and throw" was also wrong, and is corrected rather than defended. Measured behaviour: a cut inside the header throws, a cut after the header decodes what arrived and returns it, and a cut yielding nothing now throws. Detecting a missing tail needs the end-of-stream page flag, which this package does not check yet, and the README now says so.
New test/truncation_test.dart sweeps truncation points instead of pinning one
percentage, because which offset produces the empty decode depends on where the
page boundaries fall. Confirmed red before the fix and green after.
The API is stable. No behaviour changes; this freezes the surface after an adversarial pass over the FFI boundary, and documents one thing that pass t
The API is stable. No behaviour changes; this freezes the surface after an adversarial pass over the FFI boundary, and documents one thing that pass turned up.
PcmAudio.duration against what you expected when the input may
be a partial download.Verified by execution and now covered by test/native_safety_test.dart: empty
input raises ArgumentError and garbage raises AudioDecodeException; a
truncated Ogg throws while a truncated MP3 returns fewer frames at the same
sample rate; repeated decode/encode does not leak (growth across batches
flattens instead of staying linear, which is the shape a leak would have);
encodeWav writes exactly a 44-byte header plus the samples; and toMono
leaves already-mono audio unchanged.
native_toolchain_c is pre-1.0 but is a build-time dependency and does not
reach the public API frozen here.
Add example/README.md for pub.dev's Example tab (it was empty). It walks through the command-line example — detect the format, decode to PCM, print an
example/README.md for pub.dev's Example tab (it was empty). It walks
through the command-line example — detect the format, decode to PCM, print an
ASCII waveform, write a WAV — with the real output against the repo's sine
fixture. Docs only.Two things that had to be settled before a 1.0.0 freezes them.
Two things that had to be settled before a 1.0.0 freezes them.
PcmAudio, AudioInfo and AudioDecodeException as final. They
are leaf types: none of them is designed to be extended or implemented, and
nothing needs to. Sealing them now is what makes the rest of 1.x safe, since
adding a method to an open class breaks anyone who implemented it, and
AudioInfo's == would otherwise be open to an asymmetric subclass. This is
the breaking part of this release: final cannot be added after 1.0.0
without a 2.0.0, while removing it later would not break anyone.Answer the question the README kept skipping: why decode in process rather than run ffmpeg. bench/vs_ffmpeg.dart measures both paths on the same file
ffmpeg. bench/vs_ffmpeg.dart measures both paths on the same
file at four clip lengths, and the README now carries the result. The point
is not that the codec is faster. Starting ffmpeg costs about 24.8 ms on an
Apple M-series laptop before it decodes anything, measured by giving it a
0.05-second clip where there is nothing to decode, and that cost is paid per
file: at one second of audio, 98% of the subprocess time is not decoding. The
decoding is in the same class either way, about 15.7 ms against 12.9 ms over
thirty seconds.…==, Set and Map uses behave, which would be a breaking change. PcmAudio keeps identity equality by design, since its samples buffer is large and mutab…
AudioInfo. It now overrides == and hashCode over
its sampleRate, channels and frameCount fields, so two values with the
same geometry compare equal, share a hashCode, deduplicate in a Set and
work as Map keys. This lands before the 1.0.0 freeze: giving a released
value type value equality afterwards would change how existing ==,
Set and Map uses behave, which would be a breaking change. PcmAudio
keeps identity equality by design, since its samples buffer is large and
mutable and a structural == would scan every sample and rehash whenever the
buffer is written. No existing fields or methods change.Fix the int32 overflow from 0.3.1 on the Ogg Vorbis path as well. ad_info_vorbis read the stream length from the final page's granule position as an u
ad_info_vorbis read the stream length from the final page's granule
position as an unsigned int and narrowed it into the caller's int out
parameter with no range check, so a stream whose granule passed 2^31 (about
13.5 hours at 44.1 kHz) wrapped to a negative value that oggInfo and
audioInfo surfaced as a negative frameCount and duration with no error.
The Vorbis info path now rejects the overflow with an AudioDecodeException,
the same guard the MP3 path already carried.Fix an int32 overflow: the MP3 decoder accumulated the per-channel sample count in a native size_t but narrowed it into the caller's int out parameter
size_t but narrowed it into the caller's int out
parameter with no overflow check. A stream whose per-channel sample count
passed 2^31 (about 13.5 hours at 44.1 kHz) wrapped to a negative value,
which mp3Info/audioInfo surfaced as a negative frameCount and
duration with no error, and which decodeMp3 could turn into an unfreed
native buffer surfaced as an unrelated ArgumentError. Both paths now
reject the overflow explicitly (AudioDecodeException) instead of
wrapping, and _decode frees the native buffer in a finally even if the
copy into Dart memory throws.Add PcmAudio.toFloat32(), PcmAudio.channel(int) and PcmAudio.toMono(). The samples were only exposed as raw interleaved Int16List, so waveform, analys
PcmAudio.toFloat32(), PcmAudio.channel(int) and PcmAudio.toMono().
The samples were only exposed as raw interleaved Int16List, so waveform,
analysis and machine-learning callers all had to write the same
divide-by-32768 loop and manual channel split. toFloat32 returns the
normalized [-1.0, 1.0] floats, channel deinterleaves one channel into a
Float32List, and toMono averages the channels into a mono PcmAudio. All
additive; existing fields and methods are unchanged.flutter pub add failed at version solving, for every package here that builds native code:
flutter pub add failed at version solving, for every package here that builds
native code:
Because every version of flutter from sdk depends on meta 1.17.0 and
native_toolchain_c >=0.19.3 depends on meta ^1.19.0, flutter from sdk is
incompatible with native_toolchain_c >=0.19.3.
hooks 2.1.0 and native_toolchain_c 0.19.3 raised their meta floor to ^1.19.0.
The Flutter SDK pins meta, so a caret on the newest version of either made the
package unresolvable in any Flutter project, while pub.dev went on advertising
Flutter support and full platform badges.
Allowing hooks >=2.0.2 and native_toolchain_c >=0.19.2 lets the solver pick a
build that works with the pinned meta; a pure-Dart project still gets the
newest. No API or behaviour change.
Caught by trying it: flutter pub add against a fresh Flutter 3.41.2 app failed
for all seven, and resolves after the change. Host tests were green throughout
and never touched this, which is the lesson: dart test on the author's machine
does not resolve against the Flutter SDK.
hooks 2.1.0 and native_toolchain_c 0.19.3 raised their
meta floor to ^1.19.0, and Flutter's SDK pins meta to 1.17.0, so
flutter pub add failed at version solving with "flutter from sdk is
incompatible". Allowing hooks >=2.0.2 and native_toolchain_c >=0.19.2
lets the solver pick a version that works with the pinned meta, while a
pure-Dart project still resolves to the newest. No API or behaviour change.Two limits, not one. The server rejects a description over 200 characters, so the last release published without complaint. The analyser scores only t
Two limits, not one. The server rejects a description over 200 characters, so
the last release published without complaint. The analyser scores only those
under 160, and quietly took ten points off the documentation section for going
over. Publishing successfully is not the same as scoring.
pub.dev renders only what the screenshots field names, so the page a reader lands on from search opened with prose while the picture sat in the reposi
pub.dev renders only what the screenshots field names, so the page a reader
lands on from search opened with prose while the picture sat in the repository
unused.
pubspec.yaml so pub.dev renders it on the package
page. It was already in the repository and the README, but pub.dev shows only
what the screenshots: field points at.Reading a track's length meant decoding the whole file: PcmAudio carried duration, and nothing else did. For four minutes of 44.1 kHz stereo that mate
Reading a track's length meant decoding the whole file: PcmAudio carried
duration, and nothing else did. For four minutes of 44.1 kHz stereo that
materializes 40 MB of samples to answer one question, which is the wrong trade
for a playlist, an upload check, or picking files to process.
Add audioInfo, oggInfo and mp3Info, returning an AudioInfo with sampleRate,
channels, frameCount and duration and allocating no PCM. Vorbis stores its
length in the container, so stb_vorbis is opened and queried. MP3 has no
total-length field, so the frame headers are still walked, but minimp3 parses a
frame and returns its sample count when the PCM pointer is null, so the
synthesis step and the output buffer both go away.
Measured on a one-second stereo fixture, warmed up and averaged: Ogg 107 us
against 511 us for a full decode, MP3 0.9 us against 217 us. Correctness is the
part that matters though, so the tests assert the reported sample rate,
channels, frame count and duration equal what decodeAudio returns, for every
fixture in both formats. 36 tests pass.
audioInfo, oggInfo and mp3Info, which return an AudioInfo with
sampleRate, channels, frameCount and duration without decoding to
PCM. Reading a track's length used to mean decoding the whole file, which for
four minutes of 44.1 kHz stereo materializes 40 MB of samples you then throw
away. The new calls allocate no PCM: Vorbis answers from the container,
and MP3 walks its frame headers with the decoder's synthesis step skipped.
Measured on a one-second stereo fixture (Apple M-series, warmed up): Ogg
107 µs against 511 µs for a full decode, MP3 0.9 µs against 217 µs. The
reported geometry is checked against decodeAudio for every test fixture.The example decoded and re-encoded, which never showed what you do with the samples. It now reduces them to a one-line waveform (peak amplitude per co
The example decoded and re-encoded, which never showed what you do with the
samples. It now reduces them to a one-line waveform (peak amplitude per column,
scaled to the loudest column), which is the primitive a waveform view or silence
detector is built on.
Lead with the concrete value and the terms people actually search, so the package's search snippet does more work in the adoption funnel.
Lead with the concrete value and the terms people actually search, so the
package's search snippet does more work in the adoption funnel.
Move the stb_vorbis and minimp3 attributions out of LICENSE into THIRD_PARTY_NOTICES.md, so LICENSE is the plain MIT text that automated license detec
LICENSE into
THIRD_PARTY_NOTICES.md, so LICENSE is the plain MIT text that automated
license detection recognises. The attributions themselves are unchanged and
still ship with the package.Ogg Vorbis decoding via stb_vorbis and MP3 decoding via minimp3, compiled from source with Dart build hooks.
decodeAudio, decodeOgg, decodeMp3, detectFormat and PcmAudio.encodeWav writes decoded PCM to a 16-bit WAV.Your coding agent can read these notes before it upgrades. Set up the MCP server →