diff --git a/CHANGELOG.md b/CHANGELOG.md
index 123e597..81b688a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,24 @@
# Changelog
+## 3.0.5
+
+* Add `repository` field and `CHANGELOG.md` to `opus_codec_dart`
+* Fix podspec version mismatch in `opus_codec_ios` and `opus_codec_macos`
+* Bump all package versions
+
+| Package | Version |
+|---------|---------|
+| opus_dart | 3.0.5 |
+| opus_flutter | 3.0.5 |
+| opus_flutter_android | 3.0.5 |
+| opus_flutter_ios | 3.0.5 |
+| opus_flutter_linux | 3.0.5 |
+| opus_flutter_macos | 3.0.5 |
+| opus_flutter_platform_interface | 3.0.5 |
+| opus_flutter_web | 3.0.5 |
+| opus_flutter_windows | 3.0.5 |
+
+
## 3.0.4
* Add Swift Package Manager support to `opus_codec_ios` and `opus_codec_macos`
diff --git a/README.md b/README.md
index 02bcabf..221aef8 100644
--- a/README.md
+++ b/README.md
@@ -10,15 +10,15 @@ This monorepo contains a federated Flutter plugin that loads libopus on each sup
| Package | Directory | Version |
|---------|-----------|---------|
-| [opus_codec](https://pub.dev/packages/opus_codec) | [opus_flutter](./opus_flutter) | 3.0.4 |
-| [opus_codec_dart](https://pub.dev/packages/opus_codec_dart) | [opus_dart](./opus_dart) | 3.0.4 |
-| [opus_codec_platform_interface](https://pub.dev/packages/opus_codec_platform_interface) | [opus_flutter_platform_interface](./opus_flutter_platform_interface) | 3.0.4 |
-| [opus_codec_android](https://pub.dev/packages/opus_codec_android) | [opus_flutter_android](./opus_flutter_android) | 3.0.4 |
-| [opus_codec_ios](https://pub.dev/packages/opus_codec_ios) | [opus_flutter_ios](./opus_flutter_ios) | 3.0.4 |
-| [opus_codec_linux](https://pub.dev/packages/opus_codec_linux) | [opus_flutter_linux](./opus_flutter_linux) | 3.0.4 |
-| [opus_codec_macos](https://pub.dev/packages/opus_codec_macos) | [opus_flutter_macos](./opus_flutter_macos) | 3.0.4 |
-| [opus_codec_web](https://pub.dev/packages/opus_codec_web) | [opus_flutter_web](./opus_flutter_web) | 3.0.4 |
-| [opus_codec_windows](https://pub.dev/packages/opus_codec_windows) | [opus_flutter_windows](./opus_flutter_windows) | 3.0.4 |
+| [opus_codec](https://pub.dev/packages/opus_codec) | [opus_flutter](./opus_flutter) | 3.0.5 |
+| [opus_codec_dart](https://pub.dev/packages/opus_codec_dart) | [opus_dart](./opus_dart) | 3.0.5 |
+| [opus_codec_platform_interface](https://pub.dev/packages/opus_codec_platform_interface) | [opus_flutter_platform_interface](./opus_flutter_platform_interface) | 3.0.5 |
+| [opus_codec_android](https://pub.dev/packages/opus_codec_android) | [opus_flutter_android](./opus_flutter_android) | 3.0.5 |
+| [opus_codec_ios](https://pub.dev/packages/opus_codec_ios) | [opus_flutter_ios](./opus_flutter_ios) | 3.0.5 |
+| [opus_codec_linux](https://pub.dev/packages/opus_codec_linux) | [opus_flutter_linux](./opus_flutter_linux) | 3.0.5 |
+| [opus_codec_macos](https://pub.dev/packages/opus_codec_macos) | [opus_flutter_macos](./opus_flutter_macos) | 3.0.5 |
+| [opus_codec_web](https://pub.dev/packages/opus_codec_web) | [opus_flutter_web](./opus_flutter_web) | 3.0.5 |
+| [opus_codec_windows](https://pub.dev/packages/opus_codec_windows) | [opus_flutter_windows](./opus_flutter_windows) | 3.0.5 |
## Platform support
@@ -32,8 +32,8 @@ Add `opus_codec` to your `pubspec.yaml`:
```yaml
dependencies:
- opus_codec: ^3.0.4
- opus_codec_dart: ^3.0.4
+ opus_codec: ^3.0.5
+ opus_codec_dart: ^3.0.5
```
Platform packages are automatically included through the federated plugin system -- you don't need to add them individually.
diff --git a/docs/architecture.md b/docs/architecture.md
index dd0888c..8fedac4 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1,305 +1,305 @@
-# Architecture
-
-This document explains how the opus_flutter library works, from high-level design decisions down to platform-specific implementation details.
-
-## Overview
-
-opus_flutter is a **federated Flutter plugin** whose sole purpose is to load the [Opus audio codec](https://opus-codec.org/) as a `DynamicLibrary` so it can be consumed by the vendored `opus_dart` package. It does not expose Opus encoding/decoding APIs directly -- that responsibility belongs to opus_dart.
-
-`opus_dart` was originally an [external package](https://github.com/EPNW/opus_dart) but has been vendored into this repository to reduce external dependency complexity and allow direct maintenance (Dart 3 compatibility fixes, type safety improvements, etc.).
-
-The plugin follows Flutter's [federated plugin architecture](https://docs.flutter.dev/packages-and-plugins/developing-packages#federated-plugins), which splits a plugin into:
-
-1. An **app-facing package** that developers add to their `pubspec.yaml`.
-2. A **platform interface** that defines the contract all implementations must satisfy.
-3. One or more **platform packages**, each containing the native code and Dart glue for a single platform.
-
-## Package Dependency Graph
-
-```mermaid
-graph TD
- A[opus_flutterapp-facing package ] --> PI[opus_flutter_platform_interface]
- A --> AN[opus_flutter_android]
- A --> IO[opus_flutter_ios]
- A --> LI[opus_flutter_linux]
- A --> MA[opus_flutter_macos]
- A --> WE[opus_flutter_web]
- A --> WI[opus_flutter_windows]
-
- AN --> PI
- IO --> PI
- LI --> PI
- MA --> PI
- WE --> PI
- WI --> PI
-
- EX[opus_flutter/example] --> A
- EX --> OD[opus_dartvendored ]
-
- style A fill:#4fc3f7,color:#000
- style PI fill:#fff9c4,color:#000
- style AN fill:#a5d6a7,color:#000
- style IO fill:#a5d6a7,color:#000
- style LI fill:#a5d6a7,color:#000
- style MA fill:#a5d6a7,color:#000
- style WE fill:#a5d6a7,color:#000
- style WI fill:#a5d6a7,color:#000
- style OD fill:#ce93d8,color:#000
- style EX fill:#b0bec5,color:#000
-```
-
-All platform packages depend on `opus_flutter_platform_interface`. The main `opus_flutter` package depends on all of them and declares the federated plugin mapping in its `pubspec.yaml`. The vendored `opus_dart` package consumes the `DynamicLibrary` returned by `opus_flutter.load()` and provides the actual encoding/decoding APIs.
-
-## Vendored opus_dart
-
-`opus_dart` lives at `opus_dart/` in the repository root. It was vendored from [EPNW/opus_dart](https://github.com/EPNW/opus_dart) (v3.0.1) and updated for Dart 3 and `wasm_ffi` 2.x compatibility.
-
-### Cross-platform FFI via proxy_ffi.dart
-
-`opus_dart` must work with both `dart:ffi` (native platforms) and `wasm_ffi` (web). This is handled by `proxy_ffi.dart`, which uses Dart conditional exports:
-
-```dart
-export 'dart:ffi' if (dart.library.js_interop) 'package:wasm_ffi/ffi.dart';
-export 'init_ffi.dart' if (dart.library.js_interop) 'init_web.dart';
-```
-
-All source files import `proxy_ffi.dart` instead of `dart:ffi` directly, getting the correct platform types resolved at compile time. This allows a single codebase for both native and web.
-
-### Initialization flow
-
-`initOpus(Object opusLib)` accepts `Object` so callers don't need to cast from the platform interface's `Future` return type. The platform-specific `createApiObject()` (from `init_ffi.dart` or `init_web.dart`, selected by the conditional export) casts to the concrete `DynamicLibrary` and creates the `ApiObject` with properly typed fields.
-
-### Type safety: why dynamic was eliminated
-
-The original `opus_dart` used `dynamic` for several fields and parameters to bridge the gap between `dart:ffi` and `web_ffi` types. This caused **runtime crashes** in modern Dart because many `dart:ffi` methods that appear to be instance methods are actually **extension methods** -- and extension methods cannot be dispatched through `dynamic`.
-
-Methods that fail through `dynamic` dispatch:
-- `Allocator.call()` -- allocates `sizeOf() * count` bytes
-- `Pointer.operator []` -- indexed byte access
-- `Pointer.asTypedList()` -- creates a typed view over native memory
-- `Pointer.value` (getter) -- reads the value at a pointer
-- `DynamicLibrary.lookupFunction<>()` -- looks up and binds a native function
-
-All fields and parameters are now statically typed (using types from `proxy_ffi.dart`), so extension methods resolve at compile time. The only `dynamic` that remains is suppressed via `// ignore_for_file` in `init_web.dart`, which is a web-only file analyzed on native where `wasm_ffi` types don't match `dart:ffi` types -- a known limitation of the conditional import pattern.
-
-## Platform Interface
-
-The core abstraction lives in `opus_flutter_platform_interface`:
-
-```dart
-abstract class OpusFlutterPlatform extends PlatformInterface {
- static OpusFlutterPlatform _instance = OpusFlutterPlatformUnsupported();
-
- static OpusFlutterPlatform get instance => _instance;
- static set instance(OpusFlutterPlatform instance) { ... }
-
- Future load() {
- throw UnimplementedError('load() has not been implemented.');
- }
-}
-```
-
-Key design points:
-
-- Extends `PlatformInterface` from `plugin_platform_interface` to enforce that implementations extend (not implement) the class.
-- Holds a static singleton `instance` that each platform package replaces at registration time.
-- The default instance is `OpusFlutterPlatformUnsupported`, which throws `UnsupportedError`.
-- The single API surface is `Future load()`, which returns a `DynamicLibrary`.
-
-The return type is `Object` rather than a specific `DynamicLibrary` because the web uses `wasm_ffi`'s `DynamicLibrary` class (a separate type from `dart:ffi`'s `DynamicLibrary`).
-
-## Plugin Registration
-
-Each platform package declares a `dartPluginClass` in its `pubspec.yaml`. Flutter automatically calls the static `registerWith()` method during app initialization, which sets `OpusFlutterPlatform.instance` to the platform-specific implementation.
-
-```mermaid
-flowchart LR
- entry[opus_flutter.dart] --> load[opus_flutter_load.dart]
- load --> pi[OpusFlutterPlatform.instance.load]
-
- subgraph Auto-registered by Flutter
- android[OpusFlutterAndroid.registerWith]
- ios[OpusFlutterIOS.registerWith]
- linux[OpusFlutterLinux.registerWith]
- macos[OpusFlutterMacOS.registerWith]
- windows[OpusFlutterWindows.registerWith]
- web[OpusFlutterWeb.registerWith]
- end
-```
-
-The main package has a single entry point (`opus_flutter_load.dart`) that delegates to `OpusFlutterPlatform.instance.load()`. No platform-specific imports or conditional exports are needed.
-
-## Platform Implementations
-
-### Android
-
-| Aspect | Detail |
-|--------|--------|
-| Language | Java (plugin stub), C (opus via CMake) |
-| Library loading | `DynamicLibrary.open('libopus.so')` |
-| Opus distribution | Built from source at Gradle build time via CMake `FetchContent` (fetches opus v1.5.2 from GitHub) |
-| Plugin class | `OpusFlutterAndroidPlugin` -- empty `FlutterPlugin` stub |
-
-The Android build uses `FetchContent` in `CMakeLists.txt` to download opus source and compile it as a shared library. This means no opus source code is checked into the repository for Android.
-
-### iOS
-
-| Aspect | Detail |
-|--------|--------|
-| Language | Swift + Objective-C (plugin stub) |
-| Library loading | `DynamicLibrary.process()` (opus is statically linked via the vendored framework) |
-| Opus distribution | Pre-built `opus.xcframework` with slices for `ios-arm64` (device) and `ios-arm64_x86_64-simulator` |
-| Build script | `build_xcframework.sh` clones opus, builds with CMake, wraps as dynamic framework, assembles xcframework |
-| Plugin class | `OpusFlutterIosPlugin` (ObjC) bridges to `SwiftOpusFlutterIosPlugin` (Swift) -- both are empty stubs |
-
-Since opus is linked into the process, `DynamicLibrary.process()` finds the symbols without needing a file path.
-
-### macOS
-
-| Aspect | Detail |
-|--------|--------|
-| Language | Swift (plugin stub) |
-| Library loading | `DynamicLibrary.process()` |
-| Opus distribution | Pre-built `opus.xcframework` with a `macos-arm64_x86_64` universal binary |
-| Build script | `build_xcframework.sh` -- same approach as iOS, targeting the `macosx` SDK |
-| Plugin class | `OpusFlutterMacosPlugin` -- empty stub importing `FlutterMacOS` |
-
-### Linux
-
-| Aspect | Detail |
-|--------|--------|
-| Language | Dart only (no native plugin class) |
-| Library loading | `DynamicLibrary.open(path)` after copying `.so` to a temp directory |
-| Opus distribution | Pre-built shared libraries (`libopus_x86_64.so.blob`, `libopus_aarch64.so.blob`) stored as Flutter assets |
-| Build method | Native + cross-compiled from Docker (`Dockerfile`, Ubuntu 20.04 base for glibc 2.31 compatibility) |
-| Fallback | If bundled binary fails to load, falls back to system `libopus.so.0` |
-| Registration | Uses `dartPluginClass: OpusFlutterLinux` with `pluginClass: none` |
-
-At runtime, the Linux implementation:
-
-1. Detects architecture via `Abi.current()` (`linuxX64` or `linuxArm64`).
-2. Copies the matching `.so.blob` asset from `rootBundle` to a temp directory.
-3. Opens the copied library with `DynamicLibrary.open()`.
-4. If any step fails, falls back to `DynamicLibrary.open('libopus.so.0')` (system library).
-
-### Windows
-
-| Aspect | Detail |
-|--------|--------|
-| Language | Dart only (no native plugin class) |
-| Library loading | `DynamicLibrary.open(path)` after copying DLL to a temp directory |
-| Opus distribution | Pre-built DLLs (`libopus_x64.dll.blob`, `libopus_x86.dll.blob`) stored as Flutter assets |
-| Build method | Cross-compiled from Linux using MinGW via Docker (`Dockerfile`) |
-| Registration | Uses `dartPluginClass: OpusFlutterWindows` with `pluginClass: none` |
-
-At runtime, the Windows implementation:
-
-1. Uses `path_provider` to get a temp directory.
-2. Copies the correct DLL (x64 or x86 based on `Platform.version`) from assets to disk.
-3. Also copies the opus license file.
-4. Opens the DLL with `DynamicLibrary.open()`.
-
-### Web
-
-| Aspect | Detail |
-|--------|--------|
-| Language | Dart (uses `wasm_ffi` and `inject_js`) |
-| Library loading | Injects `libopus.js`, loads `libopus.wasm`, returns `wasm_ffi` `DynamicLibrary` |
-| Opus distribution | Pre-built `libopus.js` + `libopus.wasm` stored as Flutter assets |
-| Build method | Compiled with Emscripten via Docker (`Dockerfile`) |
-| Registration | Uses `pluginClass: OpusFlutterWeb` with `fileName: opus_flutter_web.dart` |
-
-The web implementation:
-
-1. Injects the Emscripten-generated JS glue (`libopus.js`) into the page.
-2. Fetches the WASM binary (`libopus.wasm`).
-3. Initializes `wasm_ffi`'s `Memory` and compiles the Emscripten module.
-4. Returns a `wasm_ffi` `DynamicLibrary.fromModule()`.
-
-## Opus Build Pipeline
-
-Each platform has a different strategy for building and distributing the opus binary:
-
-```mermaid
-flowchart TB
- subgraph Source
- GH[github.com/xiph/opus tag v1.5.2]
- end
-
- subgraph Android
- GH -->|CMake FetchContent at Gradle build time| AS[libopus.sobuilt on developer machine ]
- end
-
- subgraph iOS
- GH -->|build_xcframework.sh clones + CMake| IF[opus.xcframeworkarm64 + simulator ]
- end
-
- subgraph macOS
- GH -->|build_xcframework.sh clones + CMake| MF[opus.xcframeworkarm64 + x86_64 universal ]
- end
-
- subgraph Linux
- GH -->|Dockerfile native + cross-compile| LF[libopus_x86_64.so libopus_aarch64.so]
- end
-
- subgraph Windows
- GH -->|Dockerfile MinGW cross-compile| WF[libopus_x64.dll libopus_x86.dll]
- end
-
- subgraph Web
- GH -->|Dockerfile Emscripten| EF[libopus.js + libopus.wasm]
- end
-```
-
-## Opus Version
-
-All platforms build from or bundle **libopus v1.5.2**, fetched from https://github.com/xiph/opus. On Linux, the system-installed version is used.
-
-## Data Flow
-
-```mermaid
-sequenceDiagram
- participant App as User Code
- participant OF as opus_flutter
- participant PI as OpusFlutterPlatform
- participant Impl as Platform Implementation
- participant OD as opus_dart
-
- App->>OF: load()
- OF->>PI: instance.load()
- PI->>Impl: load()
-
- alt Android
- Impl-->>PI: DynamicLibrary.open('libopus.so')
- else iOS / macOS
- Impl-->>PI: DynamicLibrary.process()
- else Linux
- Impl->>Impl: Copy .so to temp dir
- Impl-->>PI: DynamicLibrary.open(path)
- else Windows
- Impl->>Impl: Copy DLL to temp dir
- Impl-->>PI: DynamicLibrary.open(path)
- else Web
- Impl->>Impl: Inject JS, load WASM
- Impl-->>PI: wasm_ffi DynamicLibrary
- end
-
- PI-->>OF: Object (DynamicLibrary)
- OF-->>App: Object
- App->>OD: initOpus(object)
- OD->>OD: Cast to DynamicLibrary, bind FFI functions
- App->>OD: Encode / Decode audio
-```
-
-## Example App
-
-The example app (`opus_flutter/example`) demonstrates:
-
-1. Loading opus via `opus_flutter.load()` (returns `Future`).
-2. Passing the result directly to `initOpus()` -- no cast needed.
-3. Reading a raw PCM audio file from assets.
-4. Streaming it through `StreamOpusEncoder` then `StreamOpusDecoder`.
-5. Wrapping the result in a WAV header.
-6. Sharing the output file via `share_plus`.
-
-The example depends on the vendored `opus_dart` via a path dependency (`path: ../../opus_dart`).
+# Architecture
+
+This document explains how the opus_flutter library works, from high-level design decisions down to platform-specific implementation details.
+
+## Overview
+
+opus_flutter is a **federated Flutter plugin** whose sole purpose is to load the [Opus audio codec](https://opus-codec.org/) as a `DynamicLibrary` so it can be consumed by the vendored `opus_dart` package. It does not expose Opus encoding/decoding APIs directly -- that responsibility belongs to opus_dart.
+
+`opus_dart` was originally an [external package](https://github.com/EPNW/opus_dart) but has been vendored into this repository to reduce external dependency complexity and allow direct maintenance (Dart 3 compatibility fixes, type safety improvements, etc.).
+
+The plugin follows Flutter's [federated plugin architecture](https://docs.flutter.dev/packages-and-plugins/developing-packages#federated-plugins), which splits a plugin into:
+
+1. An **app-facing package** that developers add to their `pubspec.yaml`.
+2. A **platform interface** that defines the contract all implementations must satisfy.
+3. One or more **platform packages**, each containing the native code and Dart glue for a single platform.
+
+## Package Dependency Graph
+
+```mermaid
+graph TD
+ A[opus_flutterapp-facing package ] --> PI[opus_flutter_platform_interface]
+ A --> AN[opus_flutter_android]
+ A --> IO[opus_flutter_ios]
+ A --> LI[opus_flutter_linux]
+ A --> MA[opus_flutter_macos]
+ A --> WE[opus_flutter_web]
+ A --> WI[opus_flutter_windows]
+
+ AN --> PI
+ IO --> PI
+ LI --> PI
+ MA --> PI
+ WE --> PI
+ WI --> PI
+
+ EX[opus_flutter/example] --> A
+ EX --> OD[opus_dartvendored ]
+
+ style A fill:#4fc3f7,color:#000
+ style PI fill:#fff9c4,color:#000
+ style AN fill:#a5d6a7,color:#000
+ style IO fill:#a5d6a7,color:#000
+ style LI fill:#a5d6a7,color:#000
+ style MA fill:#a5d6a7,color:#000
+ style WE fill:#a5d6a7,color:#000
+ style WI fill:#a5d6a7,color:#000
+ style OD fill:#ce93d8,color:#000
+ style EX fill:#b0bec5,color:#000
+```
+
+All platform packages depend on `opus_flutter_platform_interface`. The main `opus_flutter` package depends on all of them and declares the federated plugin mapping in its `pubspec.yaml`. The vendored `opus_dart` package consumes the `DynamicLibrary` returned by `opus_flutter.load()` and provides the actual encoding/decoding APIs.
+
+## Vendored opus_dart
+
+`opus_dart` lives at `opus_dart/` in the repository root. It was vendored from [EPNW/opus_dart](https://github.com/EPNW/opus_dart) (v3.0.1) and updated for Dart 3 and `wasm_ffi` 2.x compatibility.
+
+### Cross-platform FFI via proxy_ffi.dart
+
+`opus_dart` must work with both `dart:ffi` (native platforms) and `wasm_ffi` (web). This is handled by `proxy_ffi.dart`, which uses Dart conditional exports:
+
+```dart
+export 'dart:ffi' if (dart.library.js_interop) 'package:wasm_ffi/ffi.dart';
+export 'init_ffi.dart' if (dart.library.js_interop) 'init_web.dart';
+```
+
+All source files import `proxy_ffi.dart` instead of `dart:ffi` directly, getting the correct platform types resolved at compile time. This allows a single codebase for both native and web.
+
+### Initialization flow
+
+`initOpus(Object opusLib)` accepts `Object` so callers don't need to cast from the platform interface's `Future` return type. The platform-specific `createApiObject()` (from `init_ffi.dart` or `init_web.dart`, selected by the conditional export) casts to the concrete `DynamicLibrary` and creates the `ApiObject` with properly typed fields.
+
+### Type safety: why dynamic was eliminated
+
+The original `opus_dart` used `dynamic` for several fields and parameters to bridge the gap between `dart:ffi` and `web_ffi` types. This caused **runtime crashes** in modern Dart because many `dart:ffi` methods that appear to be instance methods are actually **extension methods** -- and extension methods cannot be dispatched through `dynamic`.
+
+Methods that fail through `dynamic` dispatch:
+- `Allocator.call()` -- allocates `sizeOf() * count` bytes
+- `Pointer.operator []` -- indexed byte access
+- `Pointer.asTypedList()` -- creates a typed view over native memory
+- `Pointer.value` (getter) -- reads the value at a pointer
+- `DynamicLibrary.lookupFunction<>()` -- looks up and binds a native function
+
+All fields and parameters are now statically typed (using types from `proxy_ffi.dart`), so extension methods resolve at compile time. The only `dynamic` that remains is suppressed via `// ignore_for_file` in `init_web.dart`, which is a web-only file analyzed on native where `wasm_ffi` types don't match `dart:ffi` types -- a known limitation of the conditional import pattern.
+
+## Platform Interface
+
+The core abstraction lives in `opus_flutter_platform_interface`:
+
+```dart
+abstract class OpusFlutterPlatform extends PlatformInterface {
+ static OpusFlutterPlatform _instance = OpusFlutterPlatformUnsupported();
+
+ static OpusFlutterPlatform get instance => _instance;
+ static set instance(OpusFlutterPlatform instance) { ... }
+
+ Future load() {
+ throw UnimplementedError('load() has not been implemented.');
+ }
+}
+```
+
+Key design points:
+
+- Extends `PlatformInterface` from `plugin_platform_interface` to enforce that implementations extend (not implement) the class.
+- Holds a static singleton `instance` that each platform package replaces at registration time.
+- The default instance is `OpusFlutterPlatformUnsupported`, which throws `UnsupportedError`.
+- The single API surface is `Future load()`, which returns a `DynamicLibrary`.
+
+The return type is `Object` rather than a specific `DynamicLibrary` because the web uses `wasm_ffi`'s `DynamicLibrary` class (a separate type from `dart:ffi`'s `DynamicLibrary`).
+
+## Plugin Registration
+
+Each platform package declares a `dartPluginClass` in its `pubspec.yaml`. Flutter automatically calls the static `registerWith()` method during app initialization, which sets `OpusFlutterPlatform.instance` to the platform-specific implementation.
+
+```mermaid
+flowchart LR
+ entry[opus_flutter.dart] --> load[opus_flutter_load.dart]
+ load --> pi[OpusFlutterPlatform.instance.load]
+
+ subgraph Auto-registered by Flutter
+ android[OpusFlutterAndroid.registerWith]
+ ios[OpusFlutterIOS.registerWith]
+ linux[OpusFlutterLinux.registerWith]
+ macos[OpusFlutterMacOS.registerWith]
+ windows[OpusFlutterWindows.registerWith]
+ web[OpusFlutterWeb.registerWith]
+ end
+```
+
+The main package has a single entry point (`opus_flutter_load.dart`) that delegates to `OpusFlutterPlatform.instance.load()`. No platform-specific imports or conditional exports are needed.
+
+## Platform Implementations
+
+### Android
+
+| Aspect | Detail |
+|--------|--------|
+| Language | Java (plugin stub), C (opus via CMake) |
+| Library loading | `DynamicLibrary.open('libopus.so')` |
+| Opus distribution | Built from source at Gradle build time via CMake `FetchContent` (fetches opus v1.5.2 from GitHub) |
+| Plugin class | `OpusFlutterAndroidPlugin` -- empty `FlutterPlugin` stub |
+
+The Android build uses `FetchContent` in `CMakeLists.txt` to download opus source and compile it as a shared library. This means no opus source code is checked into the repository for Android.
+
+### iOS
+
+| Aspect | Detail |
+|--------|--------|
+| Language | Swift + Objective-C (plugin stub) |
+| Library loading | `DynamicLibrary.process()` (opus is statically linked via the vendored framework) |
+| Opus distribution | Pre-built `opus.xcframework` with slices for `ios-arm64` (device) and `ios-arm64_x86_64-simulator` |
+| Build script | `build_xcframework.sh` clones opus, builds with CMake, wraps as dynamic framework, assembles xcframework |
+| Plugin class | `OpusFlutterIosPlugin` (ObjC) bridges to `SwiftOpusFlutterIosPlugin` (Swift) -- both are empty stubs |
+
+Since opus is linked into the process, `DynamicLibrary.process()` finds the symbols without needing a file path.
+
+### macOS
+
+| Aspect | Detail |
+|--------|--------|
+| Language | Swift (plugin stub) |
+| Library loading | `DynamicLibrary.process()` |
+| Opus distribution | Pre-built `opus.xcframework` with a `macos-arm64_x86_64` universal binary |
+| Build script | `build_xcframework.sh` -- same approach as iOS, targeting the `macosx` SDK |
+| Plugin class | `OpusFlutterMacosPlugin` -- empty stub importing `FlutterMacOS` |
+
+### Linux
+
+| Aspect | Detail |
+|--------|--------|
+| Language | Dart only (no native plugin class) |
+| Library loading | `DynamicLibrary.open(path)` after copying `.so` to a temp directory |
+| Opus distribution | Pre-built shared libraries (`libopus_x86_64.so.blob`, `libopus_aarch64.so.blob`) stored as Flutter assets |
+| Build method | Native + cross-compiled from Docker (`Dockerfile`, Ubuntu 20.04 base for glibc 2.31 compatibility) |
+| Fallback | If bundled binary fails to load, falls back to system `libopus.so.0` |
+| Registration | Uses `dartPluginClass: OpusFlutterLinux` with `pluginClass: none` |
+
+At runtime, the Linux implementation:
+
+1. Detects architecture via `Abi.current()` (`linuxX64` or `linuxArm64`).
+2. Copies the matching `.so.blob` asset from `rootBundle` to a temp directory.
+3. Opens the copied library with `DynamicLibrary.open()`.
+4. If any step fails, falls back to `DynamicLibrary.open('libopus.so.0')` (system library).
+
+### Windows
+
+| Aspect | Detail |
+|--------|--------|
+| Language | Dart only (no native plugin class) |
+| Library loading | `DynamicLibrary.open(path)` after copying DLL to a temp directory |
+| Opus distribution | Pre-built DLLs (`libopus_x64.dll.blob`, `libopus_x86.dll.blob`) stored as Flutter assets |
+| Build method | Cross-compiled from Linux using MinGW via Docker (`Dockerfile`) |
+| Registration | Uses `dartPluginClass: OpusFlutterWindows` with `pluginClass: none` |
+
+At runtime, the Windows implementation:
+
+1. Uses `path_provider` to get a temp directory.
+2. Copies the correct DLL (x64 or x86 based on `Platform.version`) from assets to disk.
+3. Also copies the opus license file.
+4. Opens the DLL with `DynamicLibrary.open()`.
+
+### Web
+
+| Aspect | Detail |
+|--------|--------|
+| Language | Dart (uses `wasm_ffi` and `inject_js`) |
+| Library loading | Injects `libopus.js`, loads `libopus.wasm`, returns `wasm_ffi` `DynamicLibrary` |
+| Opus distribution | Pre-built `libopus.js` + `libopus.wasm` stored as Flutter assets |
+| Build method | Compiled with Emscripten via Docker (`Dockerfile`) |
+| Registration | Uses `pluginClass: OpusFlutterWeb` with `fileName: opus_flutter_web.dart` |
+
+The web implementation:
+
+1. Injects the Emscripten-generated JS glue (`libopus.js`) into the page.
+2. Fetches the WASM binary (`libopus.wasm`).
+3. Initializes `wasm_ffi`'s `Memory` and compiles the Emscripten module.
+4. Returns a `wasm_ffi` `DynamicLibrary.fromModule()`.
+
+## Opus Build Pipeline
+
+Each platform has a different strategy for building and distributing the opus binary:
+
+```mermaid
+flowchart TB
+ subgraph Source
+ GH[github.com/xiph/opus tag v1.5.2]
+ end
+
+ subgraph Android
+ GH -->|CMake FetchContent at Gradle build time| AS[libopus.sobuilt on developer machine ]
+ end
+
+ subgraph iOS
+ GH -->|build_xcframework.sh clones + CMake| IF[opus.xcframeworkarm64 + simulator ]
+ end
+
+ subgraph macOS
+ GH -->|build_xcframework.sh clones + CMake| MF[opus.xcframeworkarm64 + x86_64 universal ]
+ end
+
+ subgraph Linux
+ GH -->|Dockerfile native + cross-compile| LF[libopus_x86_64.so libopus_aarch64.so]
+ end
+
+ subgraph Windows
+ GH -->|Dockerfile MinGW cross-compile| WF[libopus_x64.dll libopus_x86.dll]
+ end
+
+ subgraph Web
+ GH -->|Dockerfile Emscripten| EF[libopus.js + libopus.wasm]
+ end
+```
+
+## Opus Version
+
+All platforms build from or bundle **libopus v1.5.2**, fetched from https://github.com/xiph/opus. On Linux, the system-installed version is used.
+
+## Data Flow
+
+```mermaid
+sequenceDiagram
+ participant App as User Code
+ participant OF as opus_flutter
+ participant PI as OpusFlutterPlatform
+ participant Impl as Platform Implementation
+ participant OD as opus_dart
+
+ App->>OF: load()
+ OF->>PI: instance.load()
+ PI->>Impl: load()
+
+ alt Android
+ Impl-->>PI: DynamicLibrary.open('libopus.so')
+ else iOS / macOS
+ Impl-->>PI: DynamicLibrary.process()
+ else Linux
+ Impl->>Impl: Copy .so to temp dir
+ Impl-->>PI: DynamicLibrary.open(path)
+ else Windows
+ Impl->>Impl: Copy DLL to temp dir
+ Impl-->>PI: DynamicLibrary.open(path)
+ else Web
+ Impl->>Impl: Inject JS, load WASM
+ Impl-->>PI: wasm_ffi DynamicLibrary
+ end
+
+ PI-->>OF: Object (DynamicLibrary)
+ OF-->>App: Object
+ App->>OD: initOpus(object)
+ OD->>OD: Cast to DynamicLibrary, bind FFI functions
+ App->>OD: Encode / Decode audio
+```
+
+## Example App
+
+The example app (`opus_flutter/example`) demonstrates:
+
+1. Loading opus via `opus_flutter.load()` (returns `Future`).
+2. Passing the result directly to `initOpus()` -- no cast needed.
+3. Reading a raw PCM audio file from assets.
+4. Streaming it through `StreamOpusEncoder` then `StreamOpusDecoder`.
+5. Wrapping the result in a WAV header.
+6. Sharing the output file via `share_plus`.
+
+The example depends on the vendored `opus_dart` via a path dependency (`path: ../../opus_dart`).
diff --git a/docs/code-quality.md b/docs/code-quality.md
index 7c44f9a..8b04702 100644
--- a/docs/code-quality.md
+++ b/docs/code-quality.md
@@ -1,328 +1,328 @@
-# Code Quality
-
-This document provides an assessment of the opus_flutter codebase's quality across multiple dimensions.
-
-## Summary
-
-```mermaid
-quadrantChart
- title Code Quality Assessment
- x-axis Low Impact --> High Impact
- y-axis Low Quality --> High Quality
- quadrant-1 Strengths
- quadrant-2 Monitor
- quadrant-3 Low Priority
- quadrant-4 Address First
- Architecture: [0.85, 0.85]
- Code clarity: [0.6, 0.8]
- Documentation: [0.5, 0.55]
- Test coverage: [0.9, 0.4]
- Consistency: [0.4, 0.75]
- Maintainability: [0.75, 0.75]
- Build system: [0.7, 0.7]
-```
-
-| Dimension | Rating | Notes |
-|-----------|--------|-------|
-| Architecture | Good | Clean federated plugin structure |
-| Code clarity | Good | Small, focused files with clear intent |
-| Documentation | Fair | Public APIs documented, some packages lack detail |
-| Test coverage | Fair | Unit tests for platform interface and registration logic |
-| Consistency | Good | Uniform patterns across all packages |
-| Maintainability | Good | Clean architecture, proper plugin registration |
-| Build system | Good | Modern AGP, deterministic native builds |
-
----
-
-## Architecture
-
-**Rating: Good**
-
-The project follows Flutter's recommended federated plugin pattern correctly:
-
-- Clear separation between the app-facing package, platform interface, and platform implementations.
-- Each package has a single responsibility.
-- The platform interface uses `PlatformInterface` from `plugin_platform_interface` with proper token verification.
-- All platform packages self-register via `dartPluginClass` and `registerWith()`.
-- A single entry point (`opus_flutter_load.dart`) delegates to the platform interface without platform-specific imports.
-
----
-
-## File-by-File Analysis
-
-### Platform Interface (`opus_flutter_platform_interface`)
-
-| File | Lines | Quality | Notes |
-|------|-------|---------|-------|
-| `opus_flutter_platform_interface.dart` | 3 | Good | Clean barrel export |
-| `opus_flutter_platform_interface.dart` (src) | 46 | Good | Proper PlatformInterface usage, clear docs |
-| `opus_flutter_platform_unsupported.dart` | 12 | Good | Appropriate default fallback |
-
-No issues. Well-structured.
-
-### Main Package (`opus_flutter`)
-
-| File | Lines | Quality | Notes |
-|------|-------|---------|-------|
-| `opus_flutter.dart` | 4 | Good | Single clean export |
-| `opus_flutter_load.dart` | 15 | Good | Simple delegation to platform interface |
-
-The main package is minimal by design -- it exports a single `load()` function that delegates to `OpusFlutterPlatform.instance.load()`. Platform registration is handled automatically by Flutter via `dartPluginClass`.
-
-### Android (`opus_flutter_android`)
-
-| File | Lines | Quality | Notes |
-|------|-------|---------|-------|
-| `opus_flutter_android.dart` | 19 | Good | Clean, self-registering via `dartPluginClass` |
-| `OpusFlutterAndroidPlugin.java` | 14 | Good | Empty stub, expected for FFI-only plugins |
-| `CMakeLists.txt` | 16 | Good | Modern FetchContent approach |
-| `build.gradle` | 59 | Good | AGP 8.7.0, compileSdk 35, Java 17 |
-
-The CMakeLists.txt is well-written and concise. The build.gradle uses AGP 8.7.0 with Java 17 compatibility. Test dependencies (`junit`, `mockito`) are included but no actual tests exist yet.
-
-### iOS (`opus_flutter_ios`)
-
-| File | Lines | Quality | Notes |
-|------|-------|---------|-------|
-| `opus_flutter_ios.dart` | 19 | Good | Clean, self-registering via `dartPluginClass` |
-| `OpusFlutterIosPlugin.swift` | 8 | Good | Minimal Swift-only stub |
-| `build_xcframework.sh` | 239 | Good | Well-structured, documented, error handling |
-
-Uses Swift-only registration (no ObjC bridge). The build script is well-written with clear sections, error checking, and cleanup.
-
-### macOS (`opus_flutter_macos`)
-
-| File | Lines | Quality | Notes |
-|------|-------|---------|-------|
-| `opus_flutter_macos.dart` | 19 | Good | Clean, self-registering via `dartPluginClass` |
-| `OpusFlutterMacosPlugin.swift` | 8 | Good | Minimal Swift-only stub |
-| `build_xcframework.sh` | 222 | Good | Adapted from iOS script, well-structured |
-
-Cleanest platform implementation. Uses Swift-only registration (no ObjC bridge).
-
-### Web (`opus_flutter_web`)
-
-| File | Lines | Quality | Notes |
-|------|-------|---------|-------|
-| `opus_flutter_web.dart` | 42 | Good | Most complex platform impl, uses actively maintained wasm_ffi |
-
-The web implementation is the most involved platform package. It has to inject JavaScript, load WASM, initialize memory, and bridge through `wasm_ffi`. The migration from the unmaintained `web_ffi` to `wasm_ffi` (v2.2.0, actively maintained) has resolved the dependency risk.
-
-### Windows (`opus_flutter_windows`)
-
-| File | Lines | Quality | Notes |
-|------|-------|---------|-------|
-| `opus_flutter_windows.dart` | 57 | Good | Asset copying logic, proper arch detection via `Abi.current()` |
-
-The Windows implementation has the most runtime logic: copying DLLs from assets to a temp directory, detecting architecture via `Abi.current()`, and loading dynamically.
-
-### Example App
-
-| File | Lines | Quality | Notes |
-|------|-------|---------|-------|
-| `main.dart` | 171 | Good | Clean demo of encoding/decoding with share functionality |
-
-The example app demonstrates a complete encode/decode pipeline with file sharing. Code style is clean with proper return types and no unnecessary overrides. Uses vendored `opus_dart` via path dependency.
-
-### Vendored opus_dart (`opus_dart`)
-
-| File | Lines | Quality | Notes |
-|------|-------|---------|-------|
-| `opus_dart.dart` | 10 | Good | Clean barrel export |
-| `proxy_ffi.dart` | 3 | Good | Conditional export: `dart:ffi` on native, `wasm_ffi/ffi.dart` on web |
-| `init_ffi.dart` | 9 | Good | Native init: casts `Object` to `DynamicLibrary`, passes `malloc` as allocator |
-| `init_web.dart` | 31 | Good | Web init: registers opaque types, uses `wasm_ffi` allocator. Cross-platform analysis artifacts suppressed via `ignore_for_file` |
-| `opus_dart_misc.dart` | 78 | Good | Core types and `initOpus()` entry point. All fields statically typed |
-| `opus_dart_encoder.dart` | 344 | Good | Simple and buffered encoder implementations |
-| `opus_dart_decoder.dart` | 503 | Good | Simple and buffered decoder implementations |
-| `opus_dart_packet.dart` | 95 | Good | Packet inspection utilities |
-| `opus_dart_streaming.dart` | 343 | Good | Stream transformers for encode/decode pipelines |
-| `wrappers/*.dart` | ~600 | Good | FFI bindings. `final` class modifier added for Dart 3 |
-
-The vendored package required several fixes for modern Dart compatibility:
-
-- **`dynamic` elimination:** The original code used `dynamic` for `ApiObject.allocator`, `ApiObject` constructor parameter, and the `_asString` helper. This caused `NoSuchMethodError` at runtime because `dart:ffi` extension methods (`Allocator.call()`, `Pointer.operator []`, `Pointer.asTypedList()`) cannot be dispatched through `dynamic`. All are now statically typed via `proxy_ffi.dart`.
-- **`Pointer.elementAt()` removed:** Replaced with `Pointer` extension's `operator []` (deprecated in Dart 3.3, removed in later SDKs).
-- **`wasm_ffi` 2.x API:** Corrected import paths (`ffi.dart` not `wasm_ffi.dart`), replaced `boundMemory` with `allocator`.
-- **Dart 3 class modifiers:** All `Opaque` subclasses marked `final`.
-
----
-
-## Dart Style and Conventions
-
-### Positive Patterns
-
-- Doc comments on all public APIs using `///` syntax.
-- `@override` annotations used (macOS package).
-- All platform packages use `dartPluginClass` for self-registration.
-- Clear package naming following Flutter conventions.
-
-### Issues Found
-
-| Issue | Location | Status |
-|-------|----------|--------|
-| ~~`new` keyword used in Dart 3 codebase~~ | Various files | Resolved |
-| ~~`void` return with `async`~~ | `example/main.dart` | Resolved |
-| ~~Empty `initState()` override~~ | `example/main.dart` | Resolved |
-| ~~Missing `@override` on `load()`~~ | Platform implementations | Resolved |
-| ~~Inconsistent quote style (double vs single)~~ | `example/pubspec.yaml` | Resolved |
-| ~~`dynamic` causing runtime extension method failures~~ | `opus_dart` (vendored) | Resolved |
-| ~~Removed `dart:ffi` API (`Pointer.elementAt`)~~ | `opus_dart` (vendored) | Resolved |
-| ~~Wrong `wasm_ffi` import paths~~ | `opus_dart` (vendored) | Resolved |
-| ~~Missing `final` on `Opaque` subclasses~~ | `opus_dart` (vendored) | Resolved |
-
----
-
-## Dependency Health
-
-```mermaid
-graph LR
- subgraph Low Risk
- A[plugin_platform_interface ^2.1.8 • Active]
- B[flutter_lints ^5.0.0 • Active]
- C[path_provider ^2.1.5 • Active]
- D[share_plus ^10.0.0 • Active]
- E[platform_info ^5.0.0 • Active]
- H[wasm_ffi ^2.1.0 • Active]
- I[ffi ^2.1.0 • Active]
- end
-
- subgraph Medium Risk
- F[inject_js ^2.1.0 • 15 months ago]
- end
-
- subgraph Vendored
- G[opus_dart v3.0.1 • in-repo]
- end
-
- style A fill:#c8e6c9,color:#000
- style B fill:#c8e6c9,color:#000
- style C fill:#c8e6c9,color:#000
- style D fill:#c8e6c9,color:#000
- style E fill:#c8e6c9,color:#000
- style F fill:#fff9c4,color:#000
- style G fill:#ce93d8,color:#000
- style H fill:#c8e6c9,color:#000
- style I fill:#c8e6c9,color:#000
-```
-
-| Dependency | Version | Last Updated | Risk |
-|------------|---------|-------------|------|
-| `plugin_platform_interface` | ^2.1.8 | Active | Low |
-| `flutter_lints` | ^5.0.0 | Active | Low |
-| `path_provider` | ^2.1.5 | Active | Low |
-| `inject_js` | ^2.1.0 | 15 months ago | Medium |
-| `wasm_ffi` | ^2.1.0 | Active | Low |
-| `ffi` | ^2.1.0 | Active | Low |
-| `opus_dart` | v3.0.1 | Vendored | None |
-| `share_plus` | ^10.0.0 | Active | Low |
-| `platform_info` | ^5.0.0 | Active | Low |
-
-`opus_dart` has been vendored into the repository, eliminating it as an external dependency risk. The migration from `web_ffi` to `wasm_ffi` previously eliminated the highest-risk dependency.
-
----
-
-## Build System Quality
-
-### Android
-- **Approach:** CMake FetchContent (downloads opus at build time).
-- **Strength:** No vendored sources; always builds from a pinned tag.
-- **Risk:** Requires internet during build; network issues or removed GitHub tags will break builds.
-- **AGP:** 8.7.0 with Java 17, compileSdk 35.
-
-### iOS
-- **Approach:** Pre-built xcframework via shell script.
-- **Strength:** Deterministic; no network needed at app build time.
-- **Risk:** Script must be re-run manually to update opus.
-
-### Linux
-- **Approach:** Pre-built shared libraries via Docker, stored as Flutter assets.
-- **Strength:** Deterministic; no network or system dependency needed at app build time. Supports x86_64 and aarch64.
-- **Risk:** None significant. Uses `ubuntu:20.04` as base image (glibc 2.31 for broad compatibility). Falls back to system `libopus.so.0` if bundled binary fails.
-
-### macOS
-- **Approach:** Same as iOS.
-- **Strength/Risk:** Same as iOS.
-
-### Windows
-- **Approach:** Cross-compiled via Docker, DLLs stored as assets.
-- **Strength:** Deterministic; no network needed at app build time.
-- **Risk:** None significant. Uses `ubuntu:24.04` as base image.
-
-### Web
-- **Approach:** Compiled via Emscripten in Docker.
-- **Strength:** Deterministic output.
-- **Risk:** Same Docker base image concern as Windows.
-
----
-
-## Lint Coverage
-
-| Package | Has `analysis_options.yaml` | Lint package |
-|---------|----------------------------|-------------|
-| opus_flutter | Yes | flutter_lints |
-| opus_flutter_platform_interface | Yes | flutter_lints |
-| opus_flutter_android | Yes | flutter_lints |
-| opus_flutter_ios | Yes | flutter_lints |
-| opus_flutter_linux | Yes | flutter_lints |
-| opus_flutter_macos | Yes | flutter_lints |
-| opus_flutter_web | Yes | flutter_lints |
-| opus_flutter_windows | Yes | flutter_lints |
-| opus_dart | Yes | `package:lints/recommended.yaml` (`constant_identifier_names` disabled for C API names) |
-| example | Yes | flutter_lints |
-
-All Flutter packages have lint configuration referencing `package:flutter_lints/flutter.yaml`. The vendored `opus_dart` is a pure Dart package and uses `package:lints/recommended.yaml` (the non-Flutter equivalent) with `constant_identifier_names` disabled since the FFI wrappers mirror upstream C API naming conventions. All files pass `dart analyze` and `dart format`.
-
----
-
-## Test Coverage
-
-| Package | Unit Tests | Widget Tests | Integration Tests |
-|---------|-----------|-------------|-------------------|
-| opus_flutter | 2 tests | None | None |
-| opus_flutter_platform_interface | 6 tests | None | None |
-| opus_flutter_android | 3 tests | None | None |
-| opus_flutter_ios | 2 tests | None | None |
-| opus_flutter_linux | 2 tests | None | None |
-| opus_flutter_macos | 2 tests | None | None |
-| opus_flutter_web | 1 test | None | None |
-| opus_flutter_windows | 2 tests | None | None |
-| opus_dart | 13 tests | None | None |
-| example | None | None | None |
-
-Unit tests cover the platform interface contract (singleton, token verification, version constant, error handling) and registration logic (`registerWith()`, class hierarchy) for each platform. Native library loading (`DynamicLibrary.open()`, `DynamicLibrary.process()`) cannot be unit tested as it requires the actual opus binary. CI runs all tests on every push.
-
-The vendored `opus_dart` has 13 unit tests covering pure-logic helpers (`maxSamplesPerPacket()`, error classes, enum completeness). FFI-dependent code (encoding/decoding) requires the actual opus library and would need integration-level tests.
-
----
-
-## Recommendations by Priority
-
-### High Priority
-
-1. ~~**Add tests**~~ -- Resolved: unit tests added for platform interface and all platform implementations.
-
-### Medium Priority
-
-2. ~~**Update Docker base images**~~ -- Resolved: Windows Dockerfile updated from `ubuntu:bionic` (18.04, EOL) to `ubuntu:24.04`.
-3. ~~**Add `opus_dart` to CI**~~ -- Resolved: dedicated `analyze-opus-dart` and `test-opus-dart` jobs added.
-4. ~~**Add `analysis_options.yaml` to `opus_dart`**~~ -- Resolved: uses `package:lints/recommended.yaml`.
-5. ~~**Fix `opus_dart` formatting**~~ -- Resolved: all files pass `dart format`.
-6. ~~**Add unit tests for `opus_dart` pure logic**~~ -- Resolved: 13 tests added.
-
-### Resolved
-
-- ~~**Add CI/CD**~~ -- GitHub Actions workflow added.
-- ~~**Add `analysis_options.yaml`**~~ -- All packages have consistent lint rules.
-- ~~**Evaluate web_ffi alternatives**~~ -- Migrated to `wasm_ffi` ^2.2.0.
-- ~~**Check if Flutter workarounds are still needed**~~ -- Removed; all platforms use `dartPluginClass`.
-- ~~**Fix Dockerfile typos**~~ -- `DEBIAN_FRONTEND` corrected.
-- ~~**Simplify iOS plugin**~~ -- Swift-only, ObjC bridge removed.
-- ~~**Remove `new` keyword**~~ -- Cleaned up across codebase.
-- ~~**Align podspec versions**~~ -- Matched to pubspec versions.
-- ~~**Add Linux support**~~ -- `opus_flutter_linux` package added.
-- ~~**Vendor opus_dart**~~ -- Copied into repository, updated for Dart 3 and `wasm_ffi` 2.x.
-- ~~**Fix `dynamic` dispatch crashes**~~ -- Eliminated all `dynamic` usage in `opus_dart` that caused `NoSuchMethodError` at runtime when `dart:ffi` extension methods were called through dynamic dispatch.
-- ~~**Fix removed `dart:ffi` APIs**~~ -- Replaced `Pointer.elementAt()` with modern `operator []`.
-- ~~**Fix `wasm_ffi` import paths**~~ -- Corrected from `wasm_ffi.dart` / `wasm_ffi_modules.dart` to `ffi.dart`.
-- ~~**Dart 3 class modifier compliance**~~ -- Added `final` to all `Opaque` subclasses in wrapper files.
+# Code Quality
+
+This document provides an assessment of the opus_flutter codebase's quality across multiple dimensions.
+
+## Summary
+
+```mermaid
+quadrantChart
+ title Code Quality Assessment
+ x-axis Low Impact --> High Impact
+ y-axis Low Quality --> High Quality
+ quadrant-1 Strengths
+ quadrant-2 Monitor
+ quadrant-3 Low Priority
+ quadrant-4 Address First
+ Architecture: [0.85, 0.85]
+ Code clarity: [0.6, 0.8]
+ Documentation: [0.5, 0.55]
+ Test coverage: [0.9, 0.4]
+ Consistency: [0.4, 0.75]
+ Maintainability: [0.75, 0.75]
+ Build system: [0.7, 0.7]
+```
+
+| Dimension | Rating | Notes |
+|-----------|--------|-------|
+| Architecture | Good | Clean federated plugin structure |
+| Code clarity | Good | Small, focused files with clear intent |
+| Documentation | Fair | Public APIs documented, some packages lack detail |
+| Test coverage | Fair | Unit tests for platform interface and registration logic |
+| Consistency | Good | Uniform patterns across all packages |
+| Maintainability | Good | Clean architecture, proper plugin registration |
+| Build system | Good | Modern AGP, deterministic native builds |
+
+---
+
+## Architecture
+
+**Rating: Good**
+
+The project follows Flutter's recommended federated plugin pattern correctly:
+
+- Clear separation between the app-facing package, platform interface, and platform implementations.
+- Each package has a single responsibility.
+- The platform interface uses `PlatformInterface` from `plugin_platform_interface` with proper token verification.
+- All platform packages self-register via `dartPluginClass` and `registerWith()`.
+- A single entry point (`opus_flutter_load.dart`) delegates to the platform interface without platform-specific imports.
+
+---
+
+## File-by-File Analysis
+
+### Platform Interface (`opus_flutter_platform_interface`)
+
+| File | Lines | Quality | Notes |
+|------|-------|---------|-------|
+| `opus_flutter_platform_interface.dart` | 3 | Good | Clean barrel export |
+| `opus_flutter_platform_interface.dart` (src) | 46 | Good | Proper PlatformInterface usage, clear docs |
+| `opus_flutter_platform_unsupported.dart` | 12 | Good | Appropriate default fallback |
+
+No issues. Well-structured.
+
+### Main Package (`opus_flutter`)
+
+| File | Lines | Quality | Notes |
+|------|-------|---------|-------|
+| `opus_flutter.dart` | 4 | Good | Single clean export |
+| `opus_flutter_load.dart` | 15 | Good | Simple delegation to platform interface |
+
+The main package is minimal by design -- it exports a single `load()` function that delegates to `OpusFlutterPlatform.instance.load()`. Platform registration is handled automatically by Flutter via `dartPluginClass`.
+
+### Android (`opus_flutter_android`)
+
+| File | Lines | Quality | Notes |
+|------|-------|---------|-------|
+| `opus_flutter_android.dart` | 19 | Good | Clean, self-registering via `dartPluginClass` |
+| `OpusFlutterAndroidPlugin.java` | 14 | Good | Empty stub, expected for FFI-only plugins |
+| `CMakeLists.txt` | 16 | Good | Modern FetchContent approach |
+| `build.gradle` | 59 | Good | AGP 8.7.0, compileSdk 35, Java 17 |
+
+The CMakeLists.txt is well-written and concise. The build.gradle uses AGP 8.7.0 with Java 17 compatibility. Test dependencies (`junit`, `mockito`) are included but no actual tests exist yet.
+
+### iOS (`opus_flutter_ios`)
+
+| File | Lines | Quality | Notes |
+|------|-------|---------|-------|
+| `opus_flutter_ios.dart` | 19 | Good | Clean, self-registering via `dartPluginClass` |
+| `OpusFlutterIosPlugin.swift` | 8 | Good | Minimal Swift-only stub |
+| `build_xcframework.sh` | 239 | Good | Well-structured, documented, error handling |
+
+Uses Swift-only registration (no ObjC bridge). The build script is well-written with clear sections, error checking, and cleanup.
+
+### macOS (`opus_flutter_macos`)
+
+| File | Lines | Quality | Notes |
+|------|-------|---------|-------|
+| `opus_flutter_macos.dart` | 19 | Good | Clean, self-registering via `dartPluginClass` |
+| `OpusFlutterMacosPlugin.swift` | 8 | Good | Minimal Swift-only stub |
+| `build_xcframework.sh` | 222 | Good | Adapted from iOS script, well-structured |
+
+Cleanest platform implementation. Uses Swift-only registration (no ObjC bridge).
+
+### Web (`opus_flutter_web`)
+
+| File | Lines | Quality | Notes |
+|------|-------|---------|-------|
+| `opus_flutter_web.dart` | 42 | Good | Most complex platform impl, uses actively maintained wasm_ffi |
+
+The web implementation is the most involved platform package. It has to inject JavaScript, load WASM, initialize memory, and bridge through `wasm_ffi`. The migration from the unmaintained `web_ffi` to `wasm_ffi` (v2.2.0, actively maintained) has resolved the dependency risk.
+
+### Windows (`opus_flutter_windows`)
+
+| File | Lines | Quality | Notes |
+|------|-------|---------|-------|
+| `opus_flutter_windows.dart` | 57 | Good | Asset copying logic, proper arch detection via `Abi.current()` |
+
+The Windows implementation has the most runtime logic: copying DLLs from assets to a temp directory, detecting architecture via `Abi.current()`, and loading dynamically.
+
+### Example App
+
+| File | Lines | Quality | Notes |
+|------|-------|---------|-------|
+| `main.dart` | 171 | Good | Clean demo of encoding/decoding with share functionality |
+
+The example app demonstrates a complete encode/decode pipeline with file sharing. Code style is clean with proper return types and no unnecessary overrides. Uses vendored `opus_dart` via path dependency.
+
+### Vendored opus_dart (`opus_dart`)
+
+| File | Lines | Quality | Notes |
+|------|-------|---------|-------|
+| `opus_dart.dart` | 10 | Good | Clean barrel export |
+| `proxy_ffi.dart` | 3 | Good | Conditional export: `dart:ffi` on native, `wasm_ffi/ffi.dart` on web |
+| `init_ffi.dart` | 9 | Good | Native init: casts `Object` to `DynamicLibrary`, passes `malloc` as allocator |
+| `init_web.dart` | 31 | Good | Web init: registers opaque types, uses `wasm_ffi` allocator. Cross-platform analysis artifacts suppressed via `ignore_for_file` |
+| `opus_dart_misc.dart` | 78 | Good | Core types and `initOpus()` entry point. All fields statically typed |
+| `opus_dart_encoder.dart` | 344 | Good | Simple and buffered encoder implementations |
+| `opus_dart_decoder.dart` | 503 | Good | Simple and buffered decoder implementations |
+| `opus_dart_packet.dart` | 95 | Good | Packet inspection utilities |
+| `opus_dart_streaming.dart` | 343 | Good | Stream transformers for encode/decode pipelines |
+| `wrappers/*.dart` | ~600 | Good | FFI bindings. `final` class modifier added for Dart 3 |
+
+The vendored package required several fixes for modern Dart compatibility:
+
+- **`dynamic` elimination:** The original code used `dynamic` for `ApiObject.allocator`, `ApiObject` constructor parameter, and the `_asString` helper. This caused `NoSuchMethodError` at runtime because `dart:ffi` extension methods (`Allocator.call()`, `Pointer.operator []`, `Pointer.asTypedList()`) cannot be dispatched through `dynamic`. All are now statically typed via `proxy_ffi.dart`.
+- **`Pointer.elementAt()` removed:** Replaced with `Pointer` extension's `operator []` (deprecated in Dart 3.3, removed in later SDKs).
+- **`wasm_ffi` 2.x API:** Corrected import paths (`ffi.dart` not `wasm_ffi.dart`), replaced `boundMemory` with `allocator`.
+- **Dart 3 class modifiers:** All `Opaque` subclasses marked `final`.
+
+---
+
+## Dart Style and Conventions
+
+### Positive Patterns
+
+- Doc comments on all public APIs using `///` syntax.
+- `@override` annotations used (macOS package).
+- All platform packages use `dartPluginClass` for self-registration.
+- Clear package naming following Flutter conventions.
+
+### Issues Found
+
+| Issue | Location | Status |
+|-------|----------|--------|
+| ~~`new` keyword used in Dart 3 codebase~~ | Various files | Resolved |
+| ~~`void` return with `async`~~ | `example/main.dart` | Resolved |
+| ~~Empty `initState()` override~~ | `example/main.dart` | Resolved |
+| ~~Missing `@override` on `load()`~~ | Platform implementations | Resolved |
+| ~~Inconsistent quote style (double vs single)~~ | `example/pubspec.yaml` | Resolved |
+| ~~`dynamic` causing runtime extension method failures~~ | `opus_dart` (vendored) | Resolved |
+| ~~Removed `dart:ffi` API (`Pointer.elementAt`)~~ | `opus_dart` (vendored) | Resolved |
+| ~~Wrong `wasm_ffi` import paths~~ | `opus_dart` (vendored) | Resolved |
+| ~~Missing `final` on `Opaque` subclasses~~ | `opus_dart` (vendored) | Resolved |
+
+---
+
+## Dependency Health
+
+```mermaid
+graph LR
+ subgraph Low Risk
+ A[plugin_platform_interface ^2.1.8 • Active]
+ B[flutter_lints ^5.0.0 • Active]
+ C[path_provider ^2.1.5 • Active]
+ D[share_plus ^10.0.0 • Active]
+ E[platform_info ^5.0.0 • Active]
+ H[wasm_ffi ^2.1.0 • Active]
+ I[ffi ^2.1.0 • Active]
+ end
+
+ subgraph Medium Risk
+ F[inject_js ^2.1.0 • 15 months ago]
+ end
+
+ subgraph Vendored
+ G[opus_dart v3.0.1 • in-repo]
+ end
+
+ style A fill:#c8e6c9,color:#000
+ style B fill:#c8e6c9,color:#000
+ style C fill:#c8e6c9,color:#000
+ style D fill:#c8e6c9,color:#000
+ style E fill:#c8e6c9,color:#000
+ style F fill:#fff9c4,color:#000
+ style G fill:#ce93d8,color:#000
+ style H fill:#c8e6c9,color:#000
+ style I fill:#c8e6c9,color:#000
+```
+
+| Dependency | Version | Last Updated | Risk |
+|------------|---------|-------------|------|
+| `plugin_platform_interface` | ^2.1.8 | Active | Low |
+| `flutter_lints` | ^5.0.0 | Active | Low |
+| `path_provider` | ^2.1.5 | Active | Low |
+| `inject_js` | ^2.1.0 | 15 months ago | Medium |
+| `wasm_ffi` | ^2.1.0 | Active | Low |
+| `ffi` | ^2.1.0 | Active | Low |
+| `opus_dart` | v3.0.1 | Vendored | None |
+| `share_plus` | ^10.0.0 | Active | Low |
+| `platform_info` | ^5.0.0 | Active | Low |
+
+`opus_dart` has been vendored into the repository, eliminating it as an external dependency risk. The migration from `web_ffi` to `wasm_ffi` previously eliminated the highest-risk dependency.
+
+---
+
+## Build System Quality
+
+### Android
+- **Approach:** CMake FetchContent (downloads opus at build time).
+- **Strength:** No vendored sources; always builds from a pinned tag.
+- **Risk:** Requires internet during build; network issues or removed GitHub tags will break builds.
+- **AGP:** 8.7.0 with Java 17, compileSdk 35.
+
+### iOS
+- **Approach:** Pre-built xcframework via shell script.
+- **Strength:** Deterministic; no network needed at app build time.
+- **Risk:** Script must be re-run manually to update opus.
+
+### Linux
+- **Approach:** Pre-built shared libraries via Docker, stored as Flutter assets.
+- **Strength:** Deterministic; no network or system dependency needed at app build time. Supports x86_64 and aarch64.
+- **Risk:** None significant. Uses `ubuntu:20.04` as base image (glibc 2.31 for broad compatibility). Falls back to system `libopus.so.0` if bundled binary fails.
+
+### macOS
+- **Approach:** Same as iOS.
+- **Strength/Risk:** Same as iOS.
+
+### Windows
+- **Approach:** Cross-compiled via Docker, DLLs stored as assets.
+- **Strength:** Deterministic; no network needed at app build time.
+- **Risk:** None significant. Uses `ubuntu:24.04` as base image.
+
+### Web
+- **Approach:** Compiled via Emscripten in Docker.
+- **Strength:** Deterministic output.
+- **Risk:** Same Docker base image concern as Windows.
+
+---
+
+## Lint Coverage
+
+| Package | Has `analysis_options.yaml` | Lint package |
+|---------|----------------------------|-------------|
+| opus_flutter | Yes | flutter_lints |
+| opus_flutter_platform_interface | Yes | flutter_lints |
+| opus_flutter_android | Yes | flutter_lints |
+| opus_flutter_ios | Yes | flutter_lints |
+| opus_flutter_linux | Yes | flutter_lints |
+| opus_flutter_macos | Yes | flutter_lints |
+| opus_flutter_web | Yes | flutter_lints |
+| opus_flutter_windows | Yes | flutter_lints |
+| opus_dart | Yes | `package:lints/recommended.yaml` (`constant_identifier_names` disabled for C API names) |
+| example | Yes | flutter_lints |
+
+All Flutter packages have lint configuration referencing `package:flutter_lints/flutter.yaml`. The vendored `opus_dart` is a pure Dart package and uses `package:lints/recommended.yaml` (the non-Flutter equivalent) with `constant_identifier_names` disabled since the FFI wrappers mirror upstream C API naming conventions. All files pass `dart analyze` and `dart format`.
+
+---
+
+## Test Coverage
+
+| Package | Unit Tests | Widget Tests | Integration Tests |
+|---------|-----------|-------------|-------------------|
+| opus_flutter | 2 tests | None | None |
+| opus_flutter_platform_interface | 6 tests | None | None |
+| opus_flutter_android | 3 tests | None | None |
+| opus_flutter_ios | 2 tests | None | None |
+| opus_flutter_linux | 2 tests | None | None |
+| opus_flutter_macos | 2 tests | None | None |
+| opus_flutter_web | 1 test | None | None |
+| opus_flutter_windows | 2 tests | None | None |
+| opus_dart | 13 tests | None | None |
+| example | None | None | None |
+
+Unit tests cover the platform interface contract (singleton, token verification, version constant, error handling) and registration logic (`registerWith()`, class hierarchy) for each platform. Native library loading (`DynamicLibrary.open()`, `DynamicLibrary.process()`) cannot be unit tested as it requires the actual opus binary. CI runs all tests on every push.
+
+The vendored `opus_dart` has 13 unit tests covering pure-logic helpers (`maxSamplesPerPacket()`, error classes, enum completeness). FFI-dependent code (encoding/decoding) requires the actual opus library and would need integration-level tests.
+
+---
+
+## Recommendations by Priority
+
+### High Priority
+
+1. ~~**Add tests**~~ -- Resolved: unit tests added for platform interface and all platform implementations.
+
+### Medium Priority
+
+2. ~~**Update Docker base images**~~ -- Resolved: Windows Dockerfile updated from `ubuntu:bionic` (18.04, EOL) to `ubuntu:24.04`.
+3. ~~**Add `opus_dart` to CI**~~ -- Resolved: dedicated `analyze-opus-dart` and `test-opus-dart` jobs added.
+4. ~~**Add `analysis_options.yaml` to `opus_dart`**~~ -- Resolved: uses `package:lints/recommended.yaml`.
+5. ~~**Fix `opus_dart` formatting**~~ -- Resolved: all files pass `dart format`.
+6. ~~**Add unit tests for `opus_dart` pure logic**~~ -- Resolved: 13 tests added.
+
+### Resolved
+
+- ~~**Add CI/CD**~~ -- GitHub Actions workflow added.
+- ~~**Add `analysis_options.yaml`**~~ -- All packages have consistent lint rules.
+- ~~**Evaluate web_ffi alternatives**~~ -- Migrated to `wasm_ffi` ^2.2.0.
+- ~~**Check if Flutter workarounds are still needed**~~ -- Removed; all platforms use `dartPluginClass`.
+- ~~**Fix Dockerfile typos**~~ -- `DEBIAN_FRONTEND` corrected.
+- ~~**Simplify iOS plugin**~~ -- Swift-only, ObjC bridge removed.
+- ~~**Remove `new` keyword**~~ -- Cleaned up across codebase.
+- ~~**Align podspec versions**~~ -- Matched to pubspec versions.
+- ~~**Add Linux support**~~ -- `opus_flutter_linux` package added.
+- ~~**Vendor opus_dart**~~ -- Copied into repository, updated for Dart 3 and `wasm_ffi` 2.x.
+- ~~**Fix `dynamic` dispatch crashes**~~ -- Eliminated all `dynamic` usage in `opus_dart` that caused `NoSuchMethodError` at runtime when `dart:ffi` extension methods were called through dynamic dispatch.
+- ~~**Fix removed `dart:ffi` APIs**~~ -- Replaced `Pointer.elementAt()` with modern `operator []`.
+- ~~**Fix `wasm_ffi` import paths**~~ -- Corrected from `wasm_ffi.dart` / `wasm_ffi_modules.dart` to `ffi.dart`.
+- ~~**Dart 3 class modifier compliance**~~ -- Added `final` to all `Opaque` subclasses in wrapper files.
diff --git a/docs/ffi-analysis.md b/docs/ffi-analysis.md
new file mode 100644
index 0000000..fdebfab
--- /dev/null
+++ b/docs/ffi-analysis.md
@@ -0,0 +1,853 @@
+# FFI and wasm_ffi Analysis
+
+This document catalogues every use of `dart:ffi`, `package:ffi`, and `package:wasm_ffi`
+across the project. Its purpose is to serve as a reference for identifying potential bugs,
+memory safety issues, and behavioral differences between native and web platforms.
+
+---
+
+## 1. Conditional FFI Abstraction Layer
+
+The project uses a single entry point to abstract over native FFI and web WASM FFI:
+
+**`opus_dart/lib/src/proxy_ffi.dart`**
+
+```dart
+export 'dart:ffi' if (dart.library.js_interop) 'package:wasm_ffi/ffi.dart';
+export 'init_ffi.dart' if (dart.library.js_interop) 'init_web.dart';
+```
+
+All downstream code imports `proxy_ffi.dart` instead of `dart:ffi` directly. This means
+every `Pointer`, `DynamicLibrary`, `Allocator`, `NativeType`, and `Opaque` reference
+resolves to either `dart:ffi` or `wasm_ffi/ffi.dart` at compile time.
+
+**Implication:** Any behavioral difference between `dart:ffi` and `wasm_ffi` (e.g.
+pointer arithmetic, `nullptr` handling, allocator semantics) silently affects all code
+below this layer.
+
+---
+
+## 2. Initialization: Native vs Web
+
+### 2.1 Native (`init_ffi.dart`)
+
+```dart
+ApiObject createApiObject(Object lib) {
+ final library = lib as DynamicLibrary;
+ return ApiObject(library, ffipackage.malloc);
+}
+```
+
+- Casts the incoming `Object` to `dart:ffi` `DynamicLibrary`.
+- Uses `package:ffi`'s `malloc` as the allocator.
+- No opaque type registration needed on native.
+
+### 2.2 Web (`init_web.dart`)
+
+```dart
+ApiObject createApiObject(Object lib) {
+ final library = lib as DynamicLibrary;
+ registerOpaqueType();
+ registerOpaqueType();
+ registerOpaqueType();
+ registerOpaqueType();
+ registerOpaqueType();
+ registerOpaqueType();
+ registerOpaqueType();
+ registerOpaqueType();
+ registerOpaqueType();
+ registerOpaqueType();
+ return ApiObject(library, library.allocator);
+}
+```
+
+- Casts to `wasm_ffi` `DynamicLibrary`.
+- Must register every `Opaque` subclass so `wasm_ffi` can handle pointer lookups.
+- Uses `library.allocator` (WASM linear memory allocator) instead of `malloc`.
+- (**Fixed** — previously `OpusRepacketizer` was registered twice and `OpusCustomMode`
+ was missing. Now all 10 opaque types are registered exactly once.)
+
+### 2.3 Differences Summary
+
+| Aspect | Native (`dart:ffi`) | Web (`wasm_ffi`) |
+|------------------------|----------------------------------|------------------------------------|
+| Allocator | `malloc` from `package:ffi` | `library.allocator` |
+| Opaque type setup | Not needed | `registerOpaqueType()` required |
+| `DynamicLibrary` source| OS-level `.so`/`.dylib`/`.dll` | Emscripten JS+WASM module |
+| `nullptr` semantics | Backed by address `0` | `wasm_ffi` emulation |
+| `free(nullptr)` | No longer called | No longer called (fixed) |
+
+---
+
+## 3. Library Loading Per Platform
+
+Each platform package only touches FFI to obtain a `DynamicLibrary`. No allocation or
+pointer work happens in these packages.
+
+| Platform | Method | Notes |
+|----------|--------|-------|
+| Android | `DynamicLibrary.open('libopus.so')` | Built by CMake via FetchContent |
+| iOS | `DynamicLibrary.process()` | Statically linked via xcframework |
+| macOS | `DynamicLibrary.process()` | Statically linked via xcframework |
+| Linux | `DynamicLibrary.open(path)` then fallback to `DynamicLibrary.open('libopus.so.0')` | Bundled `.so` copied to temp dir |
+| Windows | `DynamicLibrary.open(libPath)` | Bundled DLL copied to temp dir |
+| Web | `DynamicLibrary.open('./assets/.../libopus.js', moduleName: 'libopus', useAsGlobal: GlobalMemory.ifNotSet)` | Emscripten module |
+
+**Risk areas in platform loading:**
+
+- **Linux/Windows:** The temp-directory copy strategy means the native library's
+ lifetime is decoupled from the app. If the temp directory is cleaned while the app
+ runs, subsequent `DynamicLibrary` uses will crash.
+- **Linux fallback:** Falls back to system `libopus.so.0` which may be a different
+ version than what the bindings expect.
+
+---
+
+## 4. Native Binding Wrappers
+
+Located in `opus_dart/lib/wrappers/`. These files define typedefs and use
+`DynamicLibrary.lookupFunction` to resolve C symbols.
+
+### 4.1 Opaque Types
+
+```dart
+final class OpusEncoder extends ffi.Opaque {}
+final class OpusDecoder extends ffi.Opaque {}
+final class OpusRepacketizer extends ffi.Opaque {}
+final class OpusMSEncoder extends ffi.Opaque {}
+final class OpusMSDecoder extends ffi.Opaque {}
+final class OpusProjectionEncoder extends ffi.Opaque {}
+final class OpusProjectionDecoder extends ffi.Opaque {}
+final class OpusCustomEncoder extends ffi.Opaque {}
+final class OpusCustomDecoder extends ffi.Opaque {}
+final class OpusCustomMode extends ffi.Opaque {}
+```
+
+These are used as type parameters for `Pointer` to represent C opaque structs.
+
+### 4.2 Encoder Bindings (`opus_encoder.dart`)
+
+Functions resolved via `lookupFunction`:
+
+| C function | Dart signature | Notes |
+|------------|---------------|-------|
+| `opus_encoder_get_size` | `int Function(int)` | |
+| `opus_encoder_create` | `Pointer Function(int, int, int, Pointer)` | Returns encoder state; error via out-pointer |
+| `opus_encoder_init` | `int Function(Pointer, int, int, int)` | |
+| `opus_encode` | `int Function(Pointer, Pointer, int, Pointer, int)` | Returns encoded byte count or error |
+| `opus_encode_float` | `int Function(Pointer, Pointer, int, Pointer, int)` | |
+| `opus_encoder_destroy` | `void Function(Pointer)` | |
+| `opus_encoder_ctl` | `int Function(Pointer, int, int)` | ~~Variadic in C, bound with fixed 3 args~~ **Fixed** — WASM ABI mismatch resolved via non-variadic C wrapper (`opus_encoder_ctl_int`); Dart binding tries wrapper first, falls back to variadic lookup on native. |
+
+**`opus_encoder_ctl` binding concern:** The C function `opus_encoder_ctl` is variadic.
+The binding hardcodes it to accept exactly `(st, request, va)` — three arguments.
+This works for CTL requests that take a single `int` argument, but CTL requests that
+take a pointer argument (e.g. `OPUS_GET_*` requests that write to an out-pointer)
+cannot be used through this binding. The `int va` parameter would need to be a pointer
+address cast to `int`, which is fragile and non-portable.
+
+~~On web/WASM, pointer addresses in the WASM linear memory space are 32-bit offsets,
+so passing them as `int` might work by accident, but this pattern is error-prone.~~
+**Fixed:** A non-variadic C wrapper (`opus_encoder_ctl_int`) is now compiled into the
+WASM module. The Dart binding (`_resolveEncoderCtl`) tries the wrapper first and falls
+back to the variadic symbol on native platforms where the ABI is compatible.
+
+### 4.3 Decoder Bindings (`opus_decoder.dart`)
+
+Functions resolved via `lookupFunction`:
+
+| C function | Dart signature |
+|------------|---------------|
+| `opus_decoder_get_size` | `int Function(int)` |
+| `opus_decoder_create` | `Pointer Function(int, int, Pointer)` |
+| `opus_decoder_init` | `int Function(Pointer, int, int)` |
+| `opus_decode` | `int Function(Pointer, Pointer, int, Pointer, int, int)` |
+| `opus_decode_float` | `int Function(Pointer, Pointer, int, Pointer, int, int)` |
+| `opus_decoder_destroy` | `void Function(Pointer)` |
+| `opus_packet_parse` | `int Function(Pointer, int, Pointer, Pointer, int, Pointer)` |
+| `opus_packet_get_bandwidth` | `int Function(Pointer)` |
+| `opus_packet_get_samples_per_frame` | `int Function(Pointer, int)` |
+| `opus_packet_get_nb_channels` | `int Function(Pointer)` |
+| `opus_packet_get_nb_frames` | `int Function(Pointer, int)` |
+| `opus_packet_get_nb_samples` | `int Function(Pointer, int, int)` |
+| `opus_decoder_get_nb_samples` | `int Function(Pointer, Pointer, int)` |
+| `opus_pcm_soft_clip` | `void Function(Pointer, int, int, Pointer)` |
+
+### 4.4 Lib Info Bindings (`opus_libinfo.dart`)
+
+| C function | Dart signature | Notes |
+|------------|---------------|-------|
+| `opus_get_version_string` | `Pointer Function()` | Returns pointer to static C string |
+| `opus_strerror` | `Pointer Function(int)` | Returns pointer to static C string |
+
+Both return `Pointer` rather than `Pointer`. The project manually walks
+the bytes to find the null terminator (see Section 5.4).
+
+---
+
+## 5. Memory Management Patterns
+
+### 5.1 The `ApiObject` and Global `opus` Variable
+
+```dart
+late ApiObject opus;
+
+class ApiObject {
+ final OpusLibInfoFunctions libinfo;
+ final OpusEncoderFunctions encoder;
+ final OpusDecoderFunctions decoder;
+ final Allocator allocator;
+ // ...
+}
+```
+
+All allocation goes through `opus.allocator.call(count)` and deallocation through
+`opus.allocator.free(pointer)`. The global `opus` is set once via `initOpus()`.
+
+**Risk:** `opus` is a `late` global. Any call before `initOpus()` throws
+`LateInitializationError`. There is no guard or descriptive error message.
+
+### 5.2 Allocation/Free Patterns
+
+The project uses two distinct patterns:
+
+#### Pattern A: Allocate-Use-Free (SimpleOpusEncoder, SimpleOpusDecoder, OpusPacketUtils)
+
+```
+allocate input buffer
+allocate output buffer
+call opus function
+try {
+ check error, copy result
+} finally {
+ free input buffer
+ free output buffer
+}
+```
+
+Every method call allocates and frees. This is safe but has higher overhead.
+
+(**Fixed** — all `Simple*` encode/decode methods and `pcmSoftClip` now wrap both
+allocations in a single `try/finally`. The second pointer is nullable and only freed
+if it was successfully allocated, ensuring the first allocation is always cleaned up.)
+
+```dart
+Pointer inputNative = opus.allocator.call(input.length);
+Pointer? outputNative;
+try {
+ inputNative.asTypedList(input.length).setAll(0, input);
+ outputNative = opus.allocator.call(maxOutputSizeBytes);
+ int outputLength = opus.encoder.opus_encode(...);
+ // ...
+} finally {
+ if (outputNative != null) opus.allocator.free(outputNative);
+ opus.allocator.free(inputNative);
+}
+```
+
+#### Pattern B: Preallocated Buffers (BufferedOpusEncoder, BufferedOpusDecoder)
+
+Buffers are allocated once in the factory constructor and freed in `destroy()`.
+
+```
+factory constructor:
+ allocate error, input, output, (softClipBuffer for decoder)
+ create opus state
+ if error: free input, output, (softClipBuffer), throw
+ finally: free error
+
+destroy():
+ if not destroyed:
+ mark destroyed
+ destroy opus state
+ free input, output, (softClipBuffer)
+```
+
+(**Fixed** — all four classes now attach a `Finalizer` in their private constructor.
+The finalizer callback captures only the raw native pointers (not `this`) and
+performs the same cleanup as `destroy()`. Calling `destroy()` explicitly detaches
+the finalizer to prevent double-cleanup. If `destroy()` is never called, the GC
+will eventually trigger the finalizer and release native resources.)
+
+**Concern in BufferedOpusEncoder factory:** If `opus_encoder_create` itself throws
+(as opposed to returning an error code), the `input` and `output` buffers leak because
+the `throw` path inside the `try` block only runs when `error.value != OPUS_OK`.
+
+### 5.3 Pointer Lifetime in Streaming (`opus_dart_streaming.dart`)
+
+(**Fixed** — output is now always copied to the Dart heap regardless of `copyOutput`.)
+
+`StreamOpusEncoder` and `StreamOpusDecoder` wrap `BufferedOpusEncoder`/`BufferedOpusDecoder`.
+Previously they exposed `copyOutput` as a parameter that controlled whether the output
+was copied or yielded as a native memory view. The `copyOutput` parameter is retained
+for API compatibility but output is now always copied via `Uint8List.fromList`.
+
+This eliminates two hazards:
+1. **Use-after-write:** With `copyOutput = false`, yielded views pointed into the
+ preallocated native buffer and would silently contain stale data after the next
+ encode/decode call.
+2. **FEC double-yield corruption:** In `StreamOpusDecoder`, when FEC recovers a lost
+ packet, two yields happen in succession (`_decodeFec(true)` + `_decodeFec(false)`).
+ With `copyOutput = false`, the first yield's data was overwritten by the second
+ decode before the consumer could process it.
+
+### 5.4 String Handling (`_asString`)
+
+(**Fixed** — `_asString` now has a `maxStringLength` (256) guard.)
+
+```dart
+String _asString(Pointer pointer) {
+ int i = 0;
+ while (i < maxStringLength && pointer[i] != 0) {
+ i++;
+ }
+ if (i == maxStringLength) {
+ throw StateError(
+ '_asString: no null terminator found within $maxStringLength bytes');
+ }
+ return utf8.decode(pointer.asTypedList(i));
+}
+```
+
+- Walks memory byte-by-byte until it finds a null terminator, up to `maxStringLength`.
+- Throws `StateError` if no null terminator is found within the limit, preventing
+ unbounded loops with invalid pointers.
+- Only used with `opus_get_version_string()` and `opus_strerror()`, which return
+ pointers to static C strings in libopus. These are well within the 256-byte limit.
+
+### 5.5 `nullptr` Usage
+
+`nullptr` is used in one context:
+
+1. **Decoder packet loss:** When `input` is `null`, `nullptr` is passed to
+ `opus_decode`/`opus_decode_float`. This is correct per the opus API
+ (null data pointer signals packet loss).
+
+(**Fixed** — previously `free(inputNative)` was called where `inputNative == nullptr`
+after a null-input decode. This was resolved as part of the "memory leak if second
+allocation throws" fix: `inputNative` is now a nullable `Pointer?` and the
+`finally` block uses `if (inputNative != null)` before calling `free`. The
+`free(nullptr)` call no longer occurs.)
+
+---
+
+## 6. Error Handling Around FFI Calls
+
+### 6.1 Error Code Checking
+
+All opus API calls that return error codes are checked:
+
+```dart
+if (result < opus_defines.OPUS_OK) {
+ throw OpusException(result);
+}
+```
+
+`OpusException` calls `opus_strerror(errorCode)` to get a human-readable message.
+
+### 6.2 Error Pointer Pattern
+
+`opus_encoder_create` and `opus_decoder_create` write an error code to an out-pointer:
+
+```dart
+Pointer error = opus.allocator.call(1);
+Pointer encoder = opus.encoder.opus_encoder_create(..., error);
+try {
+ if (error.value != opus_defines.OPUS_OK) {
+ throw OpusException(error.value);
+ }
+} finally {
+ opus.allocator.free(error);
+}
+```
+
+The error pointer is always freed in `finally`, which is correct.
+
+### 6.3 Destroyed State
+
+Encoder and decoder classes track `_destroyed` to prevent double-destroy:
+
+```dart
+void destroy() {
+ if (!_destroyed) {
+ _destroyed = true;
+ opus.encoder.opus_encoder_destroy(_opusEncoder);
+ }
+}
+```
+
+All public methods (`encode`, `encodeFloat`, `decode`, `decodeFloat`, `encoderCtl`,
+`pcmSoftClipOutputBuffer`) now check `_destroyed` at the top and throw
+`OpusDestroyedError` before touching any native pointer. This matches the contract
+documented in the abstract base classes. (**Fixed** — previously these methods had no
+guard and would pass dangling pointers to opus after `destroy()`.)
+
+---
+
+## 7. Pointer Type Usage Inventory
+
+| Pointer type | Where used | Purpose |
+|-------------|-----------|---------|
+| `Pointer` | encoder implementations | Opaque encoder state |
+| `Pointer` | decoder implementations | Opaque decoder state |
+| `Pointer` | create functions | Error out-pointer (1 element) |
+| `Pointer` | encode/decode | s16le PCM sample buffer |
+| `Pointer` | encode_float/decode_float | Float PCM sample buffer, soft clip state |
+| `Pointer` | encode output, decode input, packet utils | Opus packet bytes, raw audio bytes |
+
+### Pointer Casting
+
+`BufferedOpusEncoder` and `BufferedOpusDecoder` allocate a single `Pointer` buffer
+and cast it to `Pointer` or `Pointer` depending on the encode/decode variant:
+
+```dart
+_inputBuffer.cast() // for encode()
+_inputBuffer.cast() // for encodeFloat()
+_outputBuffer.cast() // for decode()
+_outputBuffer.cast() // for decodeFloat()
+```
+
+**Risk:** If the buffer byte count is not a multiple of the target type's size
+(2 for Int16, 4 for Float), the `asTypedList` call after casting will include
+partial elements or read past the intended boundary. The buffer size calculations
+use `maxSamplesPerPacket` which accounts for channel count and sample rate, so this
+should be safe in practice, but there is no runtime assertion.
+
+---
+
+## 8. `BufferedOpusDecoder` Output Buffer Sizing
+
+(**Fixed** — both issues below have been corrected.)
+
+The `BufferedOpusDecoder` factory previously defaulted `maxOutputBufferSizeBytes` to
+`maxSamplesPerPacket(sampleRate, channels)` — a sample count, not a byte count. Since
+the buffer must accommodate float output (4 bytes/sample), this was 4x too small for
+`decodeFloat` with maximum-length opus frames (120ms). Fixed to
+`4 * maxSamplesPerPacket(sampleRate, channels)`.
+
+`StreamOpusDecoder` previously computed
+`(floats ? 2 : 4) * maxSamplesPerPacket(...)` — inverted multipliers that allocated
+less space for float (which needs more). Fixed to `(floats ? 4 : 2)`.
+
+---
+
+## 9. `opus_encoder_ctl` Variadic Binding — **Fixed**
+
+The C function `opus_encoder_ctl(OpusEncoder *st, int request, ...)` is variadic.
+The Dart binding defines it with a fixed signature:
+
+```dart
+int opus_encoder_ctl(Pointer st, int request, int va);
+```
+
+This works for setter-style CTLs like `OPUS_SET_BITRATE(value)` where the third
+argument is an integer.
+
+**WASM ABI fix:** A non-variadic C wrapper (`opus_encoder_ctl_int`) is compiled into
+the WASM module (`opus_ctl_wrapper.c`), exported alongside the original function, and
+the Dart binding (`FunctionsAndGlobals._resolveEncoderCtl`) tries the wrapper symbol
+first, falling back to the variadic symbol on native platforms.
+
+Remaining limitations:
+
+- **Getter-style CTLs** (e.g. `OPUS_GET_BITRATE`) expect a `Pointer` as the
+ third argument. Passing a pointer address as `int` is technically possible but
+ non-portable and bypasses Dart's type safety.
+- **No decoder_ctl:** There is no `opus_decoder_ctl` binding at all.
+
+---
+
+## 10. Potential Bugs and Risk Summary
+
+| # | Risk | Location | Severity | Detail |
+|---|------|----------|----------|--------|
+| 1 | ~~**Duplicate `registerOpaqueType` / missing `OpusCustomMode`**~~ | `init_web.dart:28` | ~~Medium~~ **Fixed** | Duplicate `OpusRepacketizer` removed; `OpusCustomMode` now registered. |
+| 2 | ~~**Memory leak if second allocation throws**~~ | `SimpleOpusEncoder.encode`, `SimpleOpusDecoder.decode`, float variants, `pcmSoftClip` | ~~Low~~ **Fixed** | All methods now wrap both allocations in `try/finally`; second pointer is nullable and only freed if allocated. |
+| 3 | ~~**No `NativeFinalizer`**~~ | All encoder/decoder classes | ~~Medium~~ **Fixed** | All classes now use `Finalizer` for GC-driven cleanup. `destroy()` detaches the finalizer to prevent double-free. |
+| 4 | ~~**Use-after-destroy (dangling pointer)**~~ | `SimpleOpusEncoder`, `SimpleOpusDecoder`, `BufferedOpusEncoder`, `BufferedOpusDecoder` | ~~High~~ **Fixed** | All public methods now check `_destroyed` and throw `OpusDestroyedError` before touching native pointers. |
+| 5 | ~~**`copyOutput = false` use-after-write**~~ | `StreamOpusEncoder`, `StreamOpusDecoder` | ~~Medium~~ **Fixed** | Output is now always copied to Dart heap regardless of `copyOutput`. |
+| 6 | ~~**`StreamOpusDecoder` FEC double-yield overwrites**~~ | `opus_dart_streaming.dart:321-327` | ~~Medium~~ **Fixed** | See #5 — always-copy eliminates the FEC double-yield corruption. |
+| 7 | ~~**Output buffer too small for float decode**~~ | `BufferedOpusDecoder` factory, `StreamOpusDecoder` constructor | ~~High~~ **Fixed** | `BufferedOpusDecoder` default now uses `4 * maxSamplesPerPacket`. `StreamOpusDecoder` multiplier corrected to `(floats ? 4 : 2)`. |
+| 8 | ~~**`free(nullptr)` on web**~~ | `SimpleOpusDecoder.decode` finally block | ~~Low~~ **Fixed** | Resolved by nullable `inputNative` — `free` is only called when the pointer is non-null. |
+| 9 | ~~**`_asString` unbounded loop**~~ | `opus_dart_misc.dart` | ~~Low~~ **Fixed** | Now bounded by `maxStringLength` (256); throws `StateError` if no null terminator found. |
+| 10 | ~~**`opus_encoder_ctl` variadic binding**~~ | `opus_encoder.dart` | ~~Low~~ **Fixed** | WASM ABI mismatch resolved via non-variadic C wrapper (`opus_encoder_ctl_int`). Getter CTLs (pointer arg) remain unsupported. |
+| 11 | **No `opus_decoder_ctl` binding** | `opus_decoder.dart` | Low | Decoder CTL operations are not exposed. |
+| 12 | **`late` global `opus` without guard** | `opus_dart_misc.dart:55` | Low | Access before `initOpus()` gives unhelpful `LateInitializationError`. |
+| 13 | **Linux/Windows temp dir library lifetime** | `opus_flutter_linux`, `opus_flutter_windows` | Low | If temp dir is cleaned while app runs, native calls will segfault. |
+
+---
+
+## 11. File-by-File FFI Reference
+
+### Files that import FFI types
+
+| File | Imports | Allocates | Frees | Calls native |
+|------|---------|-----------|-------|-------------|
+| `opus_dart/lib/src/proxy_ffi.dart` | conditional re-export | - | - | - |
+| `opus_dart/lib/src/init_ffi.dart` | `dart:ffi`, `package:ffi` | - | - | - |
+| `opus_dart/lib/src/init_web.dart` | `wasm_ffi/ffi.dart` | - | - | `registerOpaqueType` |
+| `opus_dart/lib/src/opus_dart_misc.dart` | via `proxy_ffi` | - | - | `opus_get_version_string`, `opus_strerror` |
+| `opus_dart/lib/src/opus_dart_encoder.dart` | via `proxy_ffi` | yes | yes | `opus_encoder_create/encode/encode_float/destroy/ctl` |
+| `opus_dart/lib/src/opus_dart_decoder.dart` | via `proxy_ffi` | yes | yes | `opus_decoder_create/decode/decode_float/destroy/pcm_soft_clip` |
+| `opus_dart/lib/src/opus_dart_packet.dart` | via `proxy_ffi` | yes | yes | `opus_packet_get_*` |
+| `opus_dart/lib/src/opus_dart_streaming.dart` | (indirect via encoder/decoder) | - | - | (indirect) |
+| `opus_dart/lib/wrappers/opus_encoder.dart` | via `proxy_ffi` as `ffi` | - | - | `lookupFunction` |
+| `opus_dart/lib/wrappers/opus_decoder.dart` | via `proxy_ffi` as `ffi` | - | - | `lookupFunction` |
+| `opus_dart/lib/wrappers/opus_libinfo.dart` | via `proxy_ffi` as `ffi` | - | - | `lookupFunction` |
+| `opus_dart/lib/wrappers/opus_repacketizer.dart` | via `proxy_ffi` as `ffi` | - | - | opaque type only |
+| `opus_dart/lib/wrappers/opus_projection.dart` | via `proxy_ffi` as `ffi` | - | - | opaque type only |
+| `opus_dart/lib/wrappers/opus_multistream.dart` | via `proxy_ffi` as `ffi` | - | - | opaque type only |
+| `opus_dart/lib/wrappers/opus_custom.dart` | via `proxy_ffi` as `ffi` | - | - | opaque type only |
+| `opus_flutter_android/lib/...` | `dart:ffi` | - | - | `DynamicLibrary.open` |
+| `opus_flutter_ios/lib/...` | `dart:ffi` | - | - | `DynamicLibrary.process` |
+| `opus_flutter_macos/lib/...` | `dart:ffi` | - | - | `DynamicLibrary.process` |
+| `opus_flutter_linux/lib/...` | `dart:ffi` | - | - | `DynamicLibrary.open` |
+| `opus_flutter_windows/lib/...` | `dart:ffi` | - | - | `DynamicLibrary.open` |
+| `opus_flutter_web/lib/...` | `wasm_ffi/ffi.dart` | - | - | `DynamicLibrary.open` (async) |
+
+### Files that define opaque types
+
+| File | Types |
+|------|-------|
+| `opus_encoder.dart` | `OpusEncoder` |
+| `opus_decoder.dart` | `OpusDecoder` |
+| `opus_repacketizer.dart` | `OpusRepacketizer` |
+| `opus_multistream.dart` | `OpusMSEncoder`, `OpusMSDecoder` |
+| `opus_projection.dart` | `OpusProjectionEncoder`, `OpusProjectionDecoder` |
+| `opus_custom.dart` | `OpusCustomEncoder`, `OpusCustomDecoder`, `OpusCustomMode` |
+
+---
+
+## 12. Cross-Reference with wasm_ffi Documentation
+
+This section audits the project against every rule and constraint documented in the
+[wasm_ffi README](https://github.com/vm75/wasm_ffi/blob/main/README.md).
+
+### 12.1 `DynamicLibrary.open` Is Async
+
+**wasm_ffi rule:** `DynamicLibrary.open` is asynchronous on web, unlike `dart:ffi`.
+
+**Project compliance:** Compliant. `OpusFlutterWeb.load()` uses `await DynamicLibrary.open(...)`:
+
+```dart
+_library ??= await DynamicLibrary.open(
+ './assets/packages/opus_codec_web/assets/libopus.js',
+ moduleName: 'libopus',
+ useAsGlobal: GlobalMemory.ifNotSet,
+);
+```
+
+### 12.2 Multiple Libraries and Memory Isolation
+
+**wasm_ffi rule:** "If more than one library is loaded, the memory will continue to
+refer to the first library. This breaks calls to later loaded libraries!" Each library
+has its own memory, so objects cannot be shared between libraries.
+
+**Project compliance:** Compliant. Only one WASM library (`libopus`) is loaded. The
+`useAsGlobal: GlobalMemory.ifNotSet` parameter sets the library's memory as the global
+`Memory` instance (only if no global is set yet), which is correct for a single-library
+application.
+
+**Risk if extended:** If a future dependency also loads a WASM library and sets global
+memory, `GlobalMemory.ifNotSet` would leave the first library's memory as global. Any
+`Pointer.fromAddress()` calls (including `nullptr`) would still reference that first
+library's memory. Since the project explicitly passes `library.allocator` through
+`ApiObject`, allocation/free operations are correctly bound to the opus library's
+memory regardless.
+
+### 12.3 Opaque Type Registration
+
+**wasm_ffi rule:** "If you extend the `Opaque` class, you must register the extended
+class using `registerOpaqueType()` before using it! Also, your class MUST NOT have
+type arguments."
+
+**Project audit of `init_web.dart`:**
+
+| Opaque subclass | Registered? | Notes |
+|----------------|-------------|-------|
+| `OpusEncoder` | Yes | |
+| `OpusDecoder` | Yes | |
+| `OpusCustomEncoder` | Yes | |
+| `OpusCustomDecoder` | Yes | |
+| `OpusMSEncoder` | Yes | |
+| `OpusMSDecoder` | Yes | |
+| `OpusProjectionEncoder` | Yes | |
+| `OpusProjectionDecoder` | Yes | |
+| `OpusRepacketizer` | Yes | |
+| `OpusCustomMode` | Yes | |
+
+None of the opaque types have type arguments, which satisfies that constraint.
+
+**Verdict:** Compliant. All 10 opaque subclasses are registered exactly once.
+(**Fixed** — previously `OpusRepacketizer` was registered twice and `OpusCustomMode`
+was missing.)
+
+### 12.4 No Type Checking on Function Lookups
+
+**wasm_ffi rule:** "The actual type argument `NF` (or `T` respectively) is not used:
+There is no type checking, if the function exported from WebAssembly has the same
+signature or amount of parameters, only the name is looked up."
+
+**Implication for this project:** On native `dart:ffi`, a `lookupFunction` with a
+mismatched C typedef will cause a compile-time or load-time error. On `wasm_ffi`, the
+C typedef is completely ignored — only the function name matters. If a Dart typedef
+has the wrong number of parameters or wrong types, the call will silently pass
+incorrect values to the WASM function, leading to memory corruption or wrong results
+rather than a clear error.
+
+**Project status:** The typedefs in `opus_encoder.dart` and `opus_decoder.dart` were
+manually written to match the opus C API. They have been stable and match the libopus
+1.5.2 API. However, there is no automated verification that these match the WASM
+exports. A signature mismatch would only manifest as silent data corruption on web
+while working correctly on native.
+
+### 12.5 Return Type Constraints
+
+**wasm_ffi rule:** Only specific return types are allowed for functions resolved via
+`lookupFunction` / `asFunction`. The allowed list includes: `int`, `double`, `bool`,
+`void`, `Pointer` for primitive types, `Pointer`, `Pointer`
+(if registered), and double-nested pointers `Pointer>`.
+
+**Audit of all return types used in the project:**
+
+| Function | Return type | Allowed? |
+|----------|------------|----------|
+| `opus_encoder_get_size` | `int` | Yes |
+| `opus_encoder_create` | `Pointer` | Yes (registered Opaque) |
+| `opus_encoder_init` | `int` | Yes |
+| `opus_encode` | `int` | Yes |
+| `opus_encode_float` | `int` | Yes |
+| `opus_encoder_destroy` | `void` | Yes |
+| `opus_encoder_ctl` | `int` | Yes |
+| `opus_decoder_get_size` | `int` | Yes |
+| `opus_decoder_create` | `Pointer` | Yes (registered Opaque) |
+| `opus_decoder_init` | `int` | Yes |
+| `opus_decode` | `int` | Yes |
+| `opus_decode_float` | `int` | Yes |
+| `opus_decoder_destroy` | `void` | Yes |
+| `opus_packet_parse` | `int` | Yes |
+| `opus_packet_get_bandwidth` | `int` | Yes |
+| `opus_packet_get_samples_per_frame` | `int` | Yes |
+| `opus_packet_get_nb_channels` | `int` | Yes |
+| `opus_packet_get_nb_frames` | `int` | Yes |
+| `opus_packet_get_nb_samples` | `int` | Yes |
+| `opus_decoder_get_nb_samples` | `int` | Yes |
+| `opus_pcm_soft_clip` | `void` | Yes |
+| `opus_get_version_string` | `Pointer` | Yes |
+| `opus_strerror` | `Pointer` | Yes |
+
+**Verdict:** All return types are within the allowed set. No issues.
+
+### 12.6 WASM Export List vs Dart Symbol Lookups
+
+**wasm_ffi rule:** Functions must be in the WASM module's `EXPORTED_FUNCTIONS` to be
+looked up. Symbols not exported will cause a runtime error on lookup.
+
+The Emscripten build (`opus_flutter_web/Dockerfile`) exports these C symbols:
+
+```
+_malloc, _free,
+_opus_get_version_string, _opus_strerror,
+_opus_encoder_get_size, _opus_encoder_create, _opus_encoder_init,
+_opus_encode, _opus_encode_float, _opus_encoder_destroy,
+_opus_encoder_ctl, _opus_encoder_ctl_int,
+_opus_decoder_get_size, _opus_decoder_create, _opus_decoder_init,
+_opus_decode, _opus_decode_float, _opus_decoder_destroy,
+_opus_packet_parse, _opus_packet_get_bandwidth,
+_opus_packet_get_samples_per_frame, _opus_packet_get_nb_channels,
+_opus_packet_get_nb_frames, _opus_packet_get_nb_samples,
+_opus_decoder_get_nb_samples, _opus_pcm_soft_clip
+```
+
+**Dart lookups in `FunctionsAndGlobals` constructors (eager):**
+
+| Symbol | Exported? | Lookup timing |
+|--------|-----------|--------------|
+| `opus_get_version_string` | Yes | Eager (constructor) |
+| `opus_strerror` | Yes | Eager (constructor) |
+| `opus_encoder_get_size` | Yes | Eager (constructor) |
+| `opus_encoder_create` | Yes | Eager (constructor) |
+| `opus_encoder_init` | Yes | Eager (constructor) |
+| `opus_encode` | Yes | Eager (constructor) |
+| `opus_encode_float` | Yes | Eager (constructor) |
+| `opus_encoder_destroy` | Yes | Eager (constructor) |
+| `opus_encoder_ctl` / `opus_encoder_ctl_int` | Yes | Lazy (`late final` via `_resolveEncoderCtl`) — tries `opus_encoder_ctl_int` first, falls back to `opus_encoder_ctl` |
+| `opus_decoder_get_size` | Yes | Eager (constructor) |
+| `opus_decoder_create` | Yes | Eager (constructor) |
+| `opus_decoder_init` | Yes | Eager (constructor) |
+| `opus_decode` | Yes | Eager (constructor) |
+| `opus_decode_float` | Yes | Eager (constructor) |
+| `opus_decoder_destroy` | Yes | Eager (constructor) |
+| `opus_packet_parse` | Yes | Eager (constructor) |
+| `opus_packet_get_bandwidth` | Yes | Eager (constructor) |
+| `opus_packet_get_samples_per_frame` | Yes | Eager (constructor) |
+| `opus_packet_get_nb_channels` | Yes | Eager (constructor) |
+| `opus_packet_get_nb_frames` | Yes | Eager (constructor) |
+| `opus_packet_get_nb_samples` | Yes | Eager (constructor) |
+| `opus_decoder_get_nb_samples` | Yes | Eager (constructor) |
+| `opus_pcm_soft_clip` | Yes | Eager (constructor) |
+
+(**Fixed** — `_opus_encoder_ctl` has been added to `EXPORTED_FUNCTIONS` in the
+Dockerfile. The symbol is now exported from the WASM binary and will be found when
+`_opus_encoder_ctlPtr` performs its lazy lookup on first use.)
+
+Note: the variadic ABI concern (see 12.7) was a separate issue, now also fixed.
+Exporting the symbol ensures the lookup succeeds; the non-variadic wrapper
+(`opus_encoder_ctl_int`) ensures correct argument passing under WASM.
+
+### 12.7 Variadic Functions Under WASM — **Fixed**
+
+**wasm_ffi context:** Emscripten compiles variadic C functions to WASM using a specific
+ABI where variadic arguments are passed via a stack-allocated buffer. The compiled WASM
+function signature may not match what a simple `lookupFunction` binding expects.
+
+`opus_encoder_ctl` in C is:
+```c
+int opus_encoder_ctl(OpusEncoder *st, int request, ...);
+```
+
+~~The Dart binding treats it as a fixed 3-argument function:~~
+~~`int Function(Pointer, int, int)`~~
+
+**Fix:** A non-variadic C wrapper (`opus_encoder_ctl_int`) is compiled into the WASM
+module via `opus_ctl_wrapper.c`. This wrapper has a fixed `(OpusEncoder*, int, int)`
+signature and internally forwards to the real variadic `opus_encoder_ctl`. The Dart
+binding (`FunctionsAndGlobals._resolveEncoderCtl`) tries `opus_encoder_ctl_int` first
+(succeeds on WASM) and falls back to `opus_encoder_ctl` (works on native due to ABI
+compatibility).
+
+On native `dart:ffi`, variadic function support was added in Dart 3.0 with specific
+annotations. The current binding bypasses this by using `lookup` + `asFunction` directly,
+which happens to work on most native platforms due to calling convention compatibility.
+The fallback in `_resolveEncoderCtl` preserves this existing behavior.
+
+### 12.8 Memory Growth and `asTypedList` Views — **Fixed**
+
+**wasm_ffi context:** The Dockerfile uses `-s ALLOW_MEMORY_GROWTH=1`. When WASM memory
+grows (e.g. due to a `malloc` that exceeds the current memory size), the underlying
+`ArrayBuffer` is replaced. Existing `TypedArray` views into the old buffer become
+**detached** (invalid).
+
+~~**Risk in this project:** The `Buffered*` implementations return `asTypedList` views:~~
+
+**Fix:** Output getters now return copies; input getters create fresh views per call.
+
+```dart
+// Output getters return copies (safe across memory growth)
+Uint8List get outputBuffer =>
+ Uint8List.fromList(_outputBuffer.asTypedList(_outputBufferIndex));
+
+// Input getters create fresh views per call (safe if not cached by consumer)
+Uint8List get inputBuffer => _inputBuffer.asTypedList(maxInputBufferSizeBytes);
+```
+
+~~If a consumer holds a reference to `inputBuffer` or `outputBuffer`, and a subsequent
+allocation triggers WASM memory growth, the held view becomes a detached `TypedArray`.~~
+
+**Remaining caveat:** `inputBuffer` still returns a native-backed view (required for
+write-through semantics). Consumers must not cache the returned view across operations
+that could trigger WASM memory growth. Each call to the getter creates a fresh, valid
+view.
+
+**Affected code paths (status):**
+
+1. ~~`BufferedOpusEncoder.inputBuffer`~~ — fresh view per call (safe if not cached).
+2. ~~`BufferedOpusEncoder.outputBuffer`~~ — **Fixed**: returns copy.
+3. ~~`BufferedOpusDecoder.inputBuffer`~~ — fresh view per call (safe if not cached).
+4. ~~`BufferedOpusDecoder.outputBuffer`~~ — **Fixed**: returns copy.
+5. ~~`BufferedOpusDecoder.outputBufferAsInt16List` / `outputBufferAsFloat32List`~~ —
+ **Fixed**: return copies.
+6. ~~`StreamOpusEncoder.bind`~~ — **Fixed**: no longer caches `inputBuffer`.
+7. `SimpleOpus*` encode/decode — short-lived views, freed in same `finally` block (low risk).
+
+### 12.9 `Memory.init()` and Global Memory
+
+**wasm_ffi rule:** "The first call you should do when you want to use wasm_ffi is
+`Memory.init()`." (The README also notes this is "now automated" in newer versions.)
+
+**Project status:** The project does **not** call `Memory.init()` explicitly. Instead,
+it relies on `DynamicLibrary.open(..., useAsGlobal: GlobalMemory.ifNotSet)` to set up
+the global memory. This appears to be the newer automated approach documented by
+`wasm_ffi`, where `DynamicLibrary.open` handles memory initialization internally.
+
+**Verdict:** Likely compliant with current `wasm_ffi` versions (`^2.1.0`). If the
+project ever downgrades or the `wasm_ffi` API changes, the missing `Memory.init()`
+could become a problem.
+
+### 12.10 `nullptr` on Web
+
+**wasm_ffi context:** `nullptr` in `wasm_ffi` is `Pointer.fromAddress(0)`. This
+requires a valid `Memory.global` to bind to. Since the project sets global memory via
+`useAsGlobal: GlobalMemory.ifNotSet`, `nullptr` should work correctly after library
+loading.
+
+**Risk:** If any code path uses `nullptr` before `initOpus()` is called (which triggers
+`DynamicLibrary.open` and sets global memory), the `Pointer.fromAddress(0)` call would
+throw because `Memory.global` is not set.
+
+The project's `late ApiObject opus` global and the initialization flow (`load()` then
+`initOpus()`) mean `nullptr` is only used in encoder/decoder methods that run after
+init. This ordering is safe.
+
+### 12.11 Emscripten Build Configuration Audit
+
+Checking the Dockerfile against `wasm_ffi` requirements:
+
+| Requirement | Status | Detail |
+|-------------|--------|--------|
+| `MODULARIZE=1` | Present | Required for `DynamicLibrary.open` |
+| `EXPORT_NAME` | `libopus` | Matches `moduleName: 'libopus'` in Dart |
+| `ALLOW_MEMORY_GROWTH=1` | Present | Required for dynamic allocation |
+| `EXPORTED_RUNTIME_METHODS=["HEAPU8"]` | Present | **Required** by `wasm_ffi` for memory access |
+| `_malloc` in `EXPORTED_FUNCTIONS` | Present | Required for allocator |
+| `_free` in `EXPORTED_FUNCTIONS` | Present | Required for allocator |
+| All used C functions exported | Yes | (**Fixed** — `_opus_encoder_ctl` was missing, now exported.) |
+
+**Verdict:** Compliant. Build configuration is correct and all used C functions are
+exported.
+
+---
+
+## 13. Web-Specific Risk Summary
+
+Risks specific to the web platform, derived from cross-referencing with `wasm_ffi`
+documentation:
+
+| # | Risk | Severity | Detail |
+|---|------|----------|--------|
+| W1 | ~~**`opus_encoder_ctl` not exported from WASM**~~ | ~~High~~ **Fixed** | `_opus_encoder_ctl` added to `EXPORTED_FUNCTIONS` in Dockerfile. |
+| W2 | ~~**Variadic `opus_encoder_ctl` ABI mismatch**~~ | ~~High~~ **Fixed** | Non-variadic C wrapper (`opus_encoder_ctl_int`) compiled into WASM module; Dart binding tries wrapper first via `_resolveEncoderCtl`. |
+| W3 | ~~**`asTypedList` views detach on memory growth**~~ | ~~Medium~~ **Fixed** | Output getters (`outputBuffer`, `outputBufferAsInt16List`, `outputBufferAsFloat32List`) now return copies. `inputBuffer` getters create fresh views per call; `StreamOpusEncoder.bind` no longer caches. |
+| W4 | ~~**`StreamOpusEncoder` caches stale buffer view**~~ | ~~Medium~~ **Fixed** | `bind()` no longer caches `_encoder.inputBuffer`; it re-fetches the view on each use, so WASM memory growth cannot leave a stale view. |
+| W5 | ~~**`OpusCustomMode` not registered**~~ | ~~Medium~~ **Fixed** | `registerOpaqueType()` was missing and `OpusRepacketizer` was registered twice. Fixed: duplicate removed, `OpusCustomMode` registered. |
+| W6 | **No function signature validation** | Low | `wasm_ffi` does not validate that Dart typedefs match WASM function signatures. A typedef error would cause silent data corruption on web while working on native. |
+| W7 | ~~**`free(nullptr)` behavior unverified**~~ | ~~Low~~ **Fixed** | Resolved by nullable `inputNative` — `free` is only called when the pointer is non-null. `free(nullptr)` no longer occurs. |
+| W8 | ~~**`Pointer[i]` indexing in `_asString`**~~ | ~~Low~~ **Fixed** | `_asString` now bounds the loop to `maxStringLength` (256) and throws `StateError` if no null terminator is found, preventing unbounded WASM memory walks. |
+
+---
+
+## 14. Combined Risk Matrix (All Platforms)
+
+Merging the original findings (Section 10) with web-specific findings (Section 13):
+
+| # | Risk | Platform | Severity | Location |
+|---|------|----------|----------|----------|
+| 1 | ~~`opus_encoder_ctl` not exported from WASM~~ | Web | ~~**High**~~ **Fixed** | `_opus_encoder_ctl` added to `EXPORTED_FUNCTIONS` in Dockerfile |
+| 2 | ~~Variadic `opus_encoder_ctl` ABI mismatch under WASM~~ | Web | ~~**High**~~ **Fixed** | Non-variadic wrapper `opus_encoder_ctl_int` + `_resolveEncoderCtl` fallback |
+| 3 | ~~Use-after-destroy (no `_destroyed` check in encode/decode)~~ | All | ~~**High**~~ **Fixed** | `SimpleOpus*`, `BufferedOpus*` — all public methods now throw `OpusDestroyedError` before touching native pointers |
+| 4 | ~~Output buffer sizing bug (samples vs bytes)~~ | All | ~~**High**~~ **Fixed** | `BufferedOpusDecoder` default now uses `4 * maxSamplesPerPacket`. `StreamOpusDecoder` multiplier corrected to `(floats ? 4 : 2)`. |
+| 5 | ~~`asTypedList` views detach on WASM memory growth~~ | Web | ~~**Medium**~~ **Fixed** | Output getters return copies; input getters create fresh views per call |
+| 6 | ~~`StreamOpusEncoder.bind` caches stale buffer view~~ | Web | ~~**Medium**~~ **Fixed** | `bind()` re-fetches `_encoder.inputBuffer` on each use |
+| 7 | ~~`OpusCustomMode` not registered on web~~ | Web | ~~**Medium**~~ **Fixed** | Duplicate `OpusRepacketizer` removed; `OpusCustomMode` now registered in `init_web.dart` |
+| 8 | ~~Duplicate `OpusRepacketizer` registration~~ | Web | ~~**Low**~~ **Fixed** | See #7 |
+| 9 | ~~No `NativeFinalizer` — leaked memory if `destroy()` skipped~~ | All | ~~**Medium**~~ **Fixed** | All classes now use `Finalizer` for GC-driven cleanup |
+| 10 | ~~`copyOutput = false` use-after-write~~ | All | ~~**Medium**~~ **Fixed** | Output always copied to Dart heap |
+| 11 | ~~FEC double-yield overwrites with `copyOutput = false`~~ | All | ~~**Medium**~~ **Fixed** | See #10 |
+| 12 | ~~Memory leak if second allocation throws~~ | All | ~~**Low**~~ **Fixed** | All `Simple*` methods and `pcmSoftClip` now wrap allocations in `try/finally` |
+| 13 | ~~`free(nullptr)` behavior on web~~ | Web | ~~**Low**~~ **Fixed** | Resolved by nullable `inputNative` null check |
+| 14 | No function signature validation on web | Web | **Low** | All `lookupFunction` calls |
+| 15 | ~~`_asString` unbounded loop~~ | All | ~~**Low**~~ **Fixed** | Now bounded by `maxStringLength`; throws `StateError` on missing terminator |
+| 16 | ~~`opus_encoder_ctl` variadic binding (native)~~ | Native | ~~**Low**~~ **Fixed** | `_resolveEncoderCtl` falls back to direct variadic lookup (ABI-compatible on native) |
+| 17 | No `opus_decoder_ctl` binding | All | **Low** | `opus_decoder.dart` |
+| 18 | `late` global `opus` without guard | All | **Low** | `opus_dart_misc.dart:55` |
+| 19 | Linux/Windows temp dir library lifetime | Native | **Low** | Platform packages |
diff --git a/docs/issues-and-improvements.md b/docs/issues-and-improvements.md
index 4b8c9da..9fca5cc 100644
--- a/docs/issues-and-improvements.md
+++ b/docs/issues-and-improvements.md
@@ -1,198 +1,198 @@
-# Issues and Improvements
-
-This document lists known issues, potential risks, and suggested improvements for the opus_flutter project.
-
-```mermaid
-graph LR
- subgraph Critical
- I1[No test coverage ✅]
- I2[No CI/CD pipeline ✅]
- end
-
- subgraph Moderate
- I3[Stale platform workarounds ✅]
- I4[web_ffi unmaintained ✅]
- I5[opus_dart unmaintained ✅]
- I6[Dockerfile typos ✅]
- I7[Missing lint configs ✅]
- end
-
- subgraph Minor
- I8[iOS ObjC bridge ✅]
- I9[Windows arch detection ✅]
- I10[Example code style ✅]
- I11[Dynamic return type ✅]
- I12[Podspec version mismatch ✅]
- end
-
- subgraph opus_dart Gaps
- I17[Not in CI ✅]
- I18[No analysis_options.yaml ✅]
- I19[No tests ✅]
- I20[Formatting issues ✅]
- I21[No README/CHANGELOG ✅]
- end
-
- subgraph Features
- I13[Linux support ✅]
- I14[Version checking ✅]
- I15[Modernize Android build ✅]
- I16[Reproducible builds]
- end
-
- style I1 fill:#ef5350,color:#fff
- style I2 fill:#ef5350,color:#fff
- style I3 fill:#ffa726,color:#000
- style I4 fill:#ffa726,color:#000
- style I5 fill:#ffa726,color:#000
- style I6 fill:#ffa726,color:#000
- style I7 fill:#ffa726,color:#000
- style I8 fill:#fff9c4,color:#000
- style I9 fill:#fff9c4,color:#000
- style I10 fill:#fff9c4,color:#000
- style I11 fill:#fff9c4,color:#000
- style I12 fill:#fff9c4,color:#000
- style I13 fill:#90caf9,color:#000
- style I14 fill:#90caf9,color:#000
- style I15 fill:#90caf9,color:#000
- style I16 fill:#90caf9,color:#000
- style I17 fill:#ffa726,color:#000
- style I18 fill:#ffa726,color:#000
- style I19 fill:#ffa726,color:#000
- style I20 fill:#fff9c4,color:#000
- style I21 fill:#fff9c4,color:#000
-```
-
-## Critical Issues
-
-### 1. ~~No test coverage~~ RESOLVED
-
-**Status:** Fixed. Added 20 unit tests across 8 packages covering:
-- Platform interface contract (singleton pattern, token verification, version constant, error handling).
-- Registration logic (`registerWith()`, class hierarchy) for all 6 platform implementations.
-- Main package delegation (`load()` delegates to platform instance, throws on unsupported platform).
-- CI workflow updated to run `flutter test` for all packages on every push.
-
-### 2. ~~No CI/CD pipeline~~ RESOLVED
-
-**Status:** Fixed. Added `.github/workflows/ci.yml` with analysis (lint + format) for all 8 packages and example app builds for Android, iOS, Linux, macOS, and Web.
-
----
-
-## Moderate Issues
-
-### 3. ~~Platform workarounds may be stale~~ RESOLVED
-
-**Status:** Fixed. Removed all registration workarounds and cross-platform imports. All platform packages now declare `dartPluginClass` in their `pubspec.yaml` and provide a static `registerWith()` method, letting Flutter handle registration automatically. The conditional export (`opus_flutter_ffi.dart` vs `opus_flutter_web.dart`) has been replaced by a single entry point (`opus_flutter_load.dart`).
-
-### 4. ~~`web_ffi` is unmaintained~~ RESOLVED
-
-**Status:** Migrated from `web_ffi` (v0.7.2, unmaintained) to `wasm_ffi` (v2.2.0, actively maintained).
-
-`wasm_ffi` is a drop-in replacement for `dart:ffi` on the web, built on top of `web_ffi` with active maintenance and modern Dart support. The API change was minimal -- `EmscriptenModule.compile` now takes a `Map` with a `'wasmBinary'` key instead of raw bytes.
-
-### 5. ~~`opus_dart` is not actively maintained~~ RESOLVED
-
-**Status:** Fixed. Vendored `opus_dart` (v3.0.1) from [EPNW/opus_dart](https://github.com/EPNW/opus_dart) directly into the repository at `opus_dart/`. This eliminates the external dependency and allows direct maintenance. The example app now uses a path dependency instead of pulling from pub.dev.
-
-Additionally, the vendored package required several fixes to work with modern Dart and the correct `wasm_ffi` version:
-
-- **Dart 3 class modifiers:** All `Opaque` subclasses in wrapper files now use the `final` modifier, required because `dart:ffi`'s `Opaque` is a `base` class.
-- **`wasm_ffi` 2.x import paths:** The package exports `ffi.dart`, not `wasm_ffi.dart`. The non-existent `wasm_ffi_modules.dart` import was removed (`registerOpaqueType` is exported from `ffi.dart` directly).
-- **Deprecated `boundMemory`:** Replaced with `allocator` on `wasm_ffi`'s `DynamicLibrary`.
-- **Removed `Pointer.elementAt()`:** Deprecated in Dart 3.3 and removed in later SDKs. Replaced with `Pointer` extension's `operator []`.
-- **Eliminated `dynamic` dispatch:** The original code used `dynamic` for several fields and parameters to bridge `dart:ffi` and `web_ffi` types. This caused runtime `NoSuchMethodError` crashes because many `dart:ffi` APIs (`Allocator.call()`, `Pointer[].operator []`, `Pointer.asTypedList()`, `DynamicLibrary.lookupFunction<>()`) are **extension methods** that cannot be dispatched through `dynamic`. All fields are now statically typed via `proxy_ffi.dart`.
-- **`initOpus()` accepts `Object`:** Callers no longer need to cast from `opus_flutter.load()`'s `Future` return type; the platform-specific `createApiObject()` handles the cast internally.
-- **Cleaned up stale headers:** Replaced "AUTOMATICALLY GENERATED FILE. DO NOT MODIFY." with "Vendored from" attribution, removed dead `subtype_of_sealed_class` lint suppressions.
-
-### 6. ~~Dockerfile typos~~ RESOLVED
-
-**Status:** Fixed. Both `opus_flutter_web/Dockerfile` and `opus_flutter_windows/Dockerfile` now use the correct `DEBIAN_FRONTEND` spelling.
-
-### 7. ~~Missing `analysis_options.yaml` in most packages~~ RESOLVED
-
-**Status:** Fixed. All 7 packages and the example app now have `analysis_options.yaml` referencing `package:flutter_lints/flutter.yaml`.
-
----
-
-## Minor Issues
-
-### 8. ~~iOS plugin uses ObjC bridge unnecessarily~~ RESOLVED
-
-**Status:** Fixed. Simplified to a single Swift file (`OpusFlutterIosPlugin.swift`), matching the macOS implementation.
-
-### 9. ~~Windows implementation has architecture detection fragility~~ RESOLVED
-
-**Status:** Fixed. Replaced `Platform.version.contains('x64')` with `Abi.current() == Abi.windowsX64` from `dart:ffi`, which is the proper API for architecture detection.
-
-### 10. ~~Example app code style issues~~ RESOLVED
-
-**Status:** Fixed. `_share` now returns `Future`, empty `initState()` override removed, and MIME type consistently uses `'audio/wav'`.
-
-### 11. ~~`load()` returns `Future`~~ RESOLVED
-
-**Status:** Fixed. Changed `Future` to `Future` across the platform interface and all platform implementations. This enforces non-null returns and improves type safety.
-
-### 12. ~~Podspec versions don't match pubspec versions~~ RESOLVED
-
-**Status:** Fixed. iOS podspec now matches pubspec at `3.0.1`, macOS podspec matches at `3.0.0`.
-
----
-
-## Vendored opus_dart Gaps
-
-### 17. ~~`opus_dart` not included in CI~~ RESOLVED
-
-**Status:** Fixed. Added a dedicated `analyze-opus-dart` job (using `dart analyze` and `dart format` since it's a pure Dart package, not a Flutter package) and a `test-opus-dart` job to the CI workflow.
-
-### 18. ~~`opus_dart` missing `analysis_options.yaml`~~ RESOLVED
-
-**Status:** Fixed. Added `analysis_options.yaml` referencing `package:lints/recommended.yaml` with `constant_identifier_names` disabled (the FFI wrappers intentionally mirror C API naming like `OPUS_SET_BITRATE_REQUEST`). Added `lints` and `test` as dev dependencies.
-
-### 19. ~~`opus_dart` has no tests~~ RESOLVED
-
-**Status:** Fixed. Added 13 unit tests covering pure-logic helpers that don't require the native opus binary:
-- `maxDataBytes` constant value.
-- `maxSamplesPerPacket()` across standard sample rates and channel counts.
-- `OpusDestroyedError` encoder/decoder message content.
-- `OpusException` error code storage.
-- `Application` and `FrameTime` enum completeness.
-
-FFI-dependent code (encoding, decoding, streaming, packet inspection) requires the actual opus library and would need integration-level tests.
-
-### 20. ~~`opus_dart` has formatting issues~~ RESOLVED
-
-**Status:** Fixed. All files now pass `dart format --set-exit-if-changed`. Additionally, all 85 lint issues from `dart analyze` were resolved (library name removal, unnecessary `this`/`const`, adjacent string concatenation, local identifier naming).
-
-### 21. ~~`opus_dart` missing README and CHANGELOG~~ RESOLVED
-
-**Status:** Fixed. Added `README.md` covering the Dart-friendly API, raw bindings, initialization with `opus_flutter`, cross-platform FFI via `proxy_ffi.dart`, and encoder CTL usage. Updated from the original EPNW README to reflect the vendored state (opus 1.5.2, `wasm_ffi` instead of `web_ffi`, `publish_to: none`, no stale external links). CHANGELOG omitted since version history is tracked in git.
-
----
-
-## Feature Improvements
-
-### 13. ~~Add Linux support~~ RESOLVED
-
-**Status:** Fixed. Created `opus_flutter_linux` package with bundled pre-built opus shared libraries for x86_64 and aarch64. The libraries are built via Docker (Ubuntu 20.04 base for glibc 2.31 compatibility) and stored as Flutter assets (`libopus_x86_64.so.blob`, `libopus_aarch64.so.blob`). At runtime, the correct binary is detected via `Abi.current()`, copied from `rootBundle` to a temp directory, and loaded with `DynamicLibrary.open()`. Falls back to system `libopus.so.0` on failure. This completes the Flutter desktop story -- all six platforms are now supported with bundled binaries.
-
-### 14. ~~Add version checking~~ RESOLVED
-
-**Status:** Fixed. Added `static const String opusVersion = '1.5.2'` to `OpusFlutterPlatform` in the platform interface. All platforms (Android, iOS, macOS, Windows, Web) bundle opus v1.5.2.
-
-### 15. ~~Modernize Android build configuration~~ RESOLVED
-
-**Status:** Fixed. Updated the plugin's `build.gradle`:
-- AGP updated from `7.3.0` to `8.7.0`.
-- `compileSdk` updated from `34` to `35`.
-- Java compatibility updated from `VERSION_1_8` to `VERSION_17`.
-- `namespace` set directly (removed conditional check).
-- Example app also updated: AGP `8.7.0`, Gradle `8.9`, Java `VERSION_17`.
-
-### 16. Bundle opus sources for reproducible builds
-
-Currently, Android fetches opus at build time via `FetchContent`, iOS/macOS use a build script that clones from GitHub, and Windows/Web use Docker. If GitHub is unavailable or the tag is removed, builds will fail.
-
-**Recommendation:**
-- Consider vendoring a source archive or using a git submodule for opus across all platforms.
-- Pin checksums for downloaded archives.
+# Issues and Improvements
+
+This document lists known issues, potential risks, and suggested improvements for the opus_flutter project.
+
+```mermaid
+graph LR
+ subgraph Critical
+ I1[No test coverage ✅]
+ I2[No CI/CD pipeline ✅]
+ end
+
+ subgraph Moderate
+ I3[Stale platform workarounds ✅]
+ I4[web_ffi unmaintained ✅]
+ I5[opus_dart unmaintained ✅]
+ I6[Dockerfile typos ✅]
+ I7[Missing lint configs ✅]
+ end
+
+ subgraph Minor
+ I8[iOS ObjC bridge ✅]
+ I9[Windows arch detection ✅]
+ I10[Example code style ✅]
+ I11[Dynamic return type ✅]
+ I12[Podspec version mismatch ✅]
+ end
+
+ subgraph opus_dart Gaps
+ I17[Not in CI ✅]
+ I18[No analysis_options.yaml ✅]
+ I19[No tests ✅]
+ I20[Formatting issues ✅]
+ I21[No README/CHANGELOG ✅]
+ end
+
+ subgraph Features
+ I13[Linux support ✅]
+ I14[Version checking ✅]
+ I15[Modernize Android build ✅]
+ I16[Reproducible builds]
+ end
+
+ style I1 fill:#ef5350,color:#fff
+ style I2 fill:#ef5350,color:#fff
+ style I3 fill:#ffa726,color:#000
+ style I4 fill:#ffa726,color:#000
+ style I5 fill:#ffa726,color:#000
+ style I6 fill:#ffa726,color:#000
+ style I7 fill:#ffa726,color:#000
+ style I8 fill:#fff9c4,color:#000
+ style I9 fill:#fff9c4,color:#000
+ style I10 fill:#fff9c4,color:#000
+ style I11 fill:#fff9c4,color:#000
+ style I12 fill:#fff9c4,color:#000
+ style I13 fill:#90caf9,color:#000
+ style I14 fill:#90caf9,color:#000
+ style I15 fill:#90caf9,color:#000
+ style I16 fill:#90caf9,color:#000
+ style I17 fill:#ffa726,color:#000
+ style I18 fill:#ffa726,color:#000
+ style I19 fill:#ffa726,color:#000
+ style I20 fill:#fff9c4,color:#000
+ style I21 fill:#fff9c4,color:#000
+```
+
+## Critical Issues
+
+### 1. ~~No test coverage~~ RESOLVED
+
+**Status:** Fixed. Added 20 unit tests across 8 packages covering:
+- Platform interface contract (singleton pattern, token verification, version constant, error handling).
+- Registration logic (`registerWith()`, class hierarchy) for all 6 platform implementations.
+- Main package delegation (`load()` delegates to platform instance, throws on unsupported platform).
+- CI workflow updated to run `flutter test` for all packages on every push.
+
+### 2. ~~No CI/CD pipeline~~ RESOLVED
+
+**Status:** Fixed. Added `.github/workflows/ci.yml` with analysis (lint + format) for all 8 packages and example app builds for Android, iOS, Linux, macOS, and Web.
+
+---
+
+## Moderate Issues
+
+### 3. ~~Platform workarounds may be stale~~ RESOLVED
+
+**Status:** Fixed. Removed all registration workarounds and cross-platform imports. All platform packages now declare `dartPluginClass` in their `pubspec.yaml` and provide a static `registerWith()` method, letting Flutter handle registration automatically. The conditional export (`opus_flutter_ffi.dart` vs `opus_flutter_web.dart`) has been replaced by a single entry point (`opus_flutter_load.dart`).
+
+### 4. ~~`web_ffi` is unmaintained~~ RESOLVED
+
+**Status:** Migrated from `web_ffi` (v0.7.2, unmaintained) to `wasm_ffi` (v2.2.0, actively maintained).
+
+`wasm_ffi` is a drop-in replacement for `dart:ffi` on the web, built on top of `web_ffi` with active maintenance and modern Dart support. The API change was minimal -- `EmscriptenModule.compile` now takes a `Map` with a `'wasmBinary'` key instead of raw bytes.
+
+### 5. ~~`opus_dart` is not actively maintained~~ RESOLVED
+
+**Status:** Fixed. Vendored `opus_dart` (v3.0.1) from [EPNW/opus_dart](https://github.com/EPNW/opus_dart) directly into the repository at `opus_dart/`. This eliminates the external dependency and allows direct maintenance. The example app now uses a path dependency instead of pulling from pub.dev.
+
+Additionally, the vendored package required several fixes to work with modern Dart and the correct `wasm_ffi` version:
+
+- **Dart 3 class modifiers:** All `Opaque` subclasses in wrapper files now use the `final` modifier, required because `dart:ffi`'s `Opaque` is a `base` class.
+- **`wasm_ffi` 2.x import paths:** The package exports `ffi.dart`, not `wasm_ffi.dart`. The non-existent `wasm_ffi_modules.dart` import was removed (`registerOpaqueType` is exported from `ffi.dart` directly).
+- **Deprecated `boundMemory`:** Replaced with `allocator` on `wasm_ffi`'s `DynamicLibrary`.
+- **Removed `Pointer.elementAt()`:** Deprecated in Dart 3.3 and removed in later SDKs. Replaced with `Pointer` extension's `operator []`.
+- **Eliminated `dynamic` dispatch:** The original code used `dynamic` for several fields and parameters to bridge `dart:ffi` and `web_ffi` types. This caused runtime `NoSuchMethodError` crashes because many `dart:ffi` APIs (`Allocator.call()`, `Pointer[].operator []`, `Pointer.asTypedList()`, `DynamicLibrary.lookupFunction<>()`) are **extension methods** that cannot be dispatched through `dynamic`. All fields are now statically typed via `proxy_ffi.dart`.
+- **`initOpus()` accepts `Object`:** Callers no longer need to cast from `opus_flutter.load()`'s `Future` return type; the platform-specific `createApiObject()` handles the cast internally.
+- **Cleaned up stale headers:** Replaced "AUTOMATICALLY GENERATED FILE. DO NOT MODIFY." with "Vendored from" attribution, removed dead `subtype_of_sealed_class` lint suppressions.
+
+### 6. ~~Dockerfile typos~~ RESOLVED
+
+**Status:** Fixed. Both `opus_flutter_web/Dockerfile` and `opus_flutter_windows/Dockerfile` now use the correct `DEBIAN_FRONTEND` spelling.
+
+### 7. ~~Missing `analysis_options.yaml` in most packages~~ RESOLVED
+
+**Status:** Fixed. All 7 packages and the example app now have `analysis_options.yaml` referencing `package:flutter_lints/flutter.yaml`.
+
+---
+
+## Minor Issues
+
+### 8. ~~iOS plugin uses ObjC bridge unnecessarily~~ RESOLVED
+
+**Status:** Fixed. Simplified to a single Swift file (`OpusFlutterIosPlugin.swift`), matching the macOS implementation.
+
+### 9. ~~Windows implementation has architecture detection fragility~~ RESOLVED
+
+**Status:** Fixed. Replaced `Platform.version.contains('x64')` with `Abi.current() == Abi.windowsX64` from `dart:ffi`, which is the proper API for architecture detection.
+
+### 10. ~~Example app code style issues~~ RESOLVED
+
+**Status:** Fixed. `_share` now returns `Future`, empty `initState()` override removed, and MIME type consistently uses `'audio/wav'`.
+
+### 11. ~~`load()` returns `Future`~~ RESOLVED
+
+**Status:** Fixed. Changed `Future` to `Future` across the platform interface and all platform implementations. This enforces non-null returns and improves type safety.
+
+### 12. ~~Podspec versions don't match pubspec versions~~ RESOLVED
+
+**Status:** Fixed. iOS podspec now matches pubspec at `3.0.1`, macOS podspec matches at `3.0.0`.
+
+---
+
+## Vendored opus_dart Gaps
+
+### 17. ~~`opus_dart` not included in CI~~ RESOLVED
+
+**Status:** Fixed. Added a dedicated `analyze-opus-dart` job (using `dart analyze` and `dart format` since it's a pure Dart package, not a Flutter package) and a `test-opus-dart` job to the CI workflow.
+
+### 18. ~~`opus_dart` missing `analysis_options.yaml`~~ RESOLVED
+
+**Status:** Fixed. Added `analysis_options.yaml` referencing `package:lints/recommended.yaml` with `constant_identifier_names` disabled (the FFI wrappers intentionally mirror C API naming like `OPUS_SET_BITRATE_REQUEST`). Added `lints` and `test` as dev dependencies.
+
+### 19. ~~`opus_dart` has no tests~~ RESOLVED
+
+**Status:** Fixed. Added 13 unit tests covering pure-logic helpers that don't require the native opus binary:
+- `maxDataBytes` constant value.
+- `maxSamplesPerPacket()` across standard sample rates and channel counts.
+- `OpusDestroyedError` encoder/decoder message content.
+- `OpusException` error code storage.
+- `Application` and `FrameTime` enum completeness.
+
+FFI-dependent code (encoding, decoding, streaming, packet inspection) requires the actual opus library and would need integration-level tests.
+
+### 20. ~~`opus_dart` has formatting issues~~ RESOLVED
+
+**Status:** Fixed. All files now pass `dart format --set-exit-if-changed`. Additionally, all 85 lint issues from `dart analyze` were resolved (library name removal, unnecessary `this`/`const`, adjacent string concatenation, local identifier naming).
+
+### 21. ~~`opus_dart` missing README and CHANGELOG~~ RESOLVED
+
+**Status:** Fixed. Added `README.md` covering the Dart-friendly API, raw bindings, initialization with `opus_flutter`, cross-platform FFI via `proxy_ffi.dart`, and encoder CTL usage. Updated from the original EPNW README to reflect the vendored state (opus 1.5.2, `wasm_ffi` instead of `web_ffi`, `publish_to: none`, no stale external links). CHANGELOG omitted since version history is tracked in git.
+
+---
+
+## Feature Improvements
+
+### 13. ~~Add Linux support~~ RESOLVED
+
+**Status:** Fixed. Created `opus_flutter_linux` package with bundled pre-built opus shared libraries for x86_64 and aarch64. The libraries are built via Docker (Ubuntu 20.04 base for glibc 2.31 compatibility) and stored as Flutter assets (`libopus_x86_64.so.blob`, `libopus_aarch64.so.blob`). At runtime, the correct binary is detected via `Abi.current()`, copied from `rootBundle` to a temp directory, and loaded with `DynamicLibrary.open()`. Falls back to system `libopus.so.0` on failure. This completes the Flutter desktop story -- all six platforms are now supported with bundled binaries.
+
+### 14. ~~Add version checking~~ RESOLVED
+
+**Status:** Fixed. Added `static const String opusVersion = '1.5.2'` to `OpusFlutterPlatform` in the platform interface. All platforms (Android, iOS, macOS, Windows, Web) bundle opus v1.5.2.
+
+### 15. ~~Modernize Android build configuration~~ RESOLVED
+
+**Status:** Fixed. Updated the plugin's `build.gradle`:
+- AGP updated from `7.3.0` to `8.7.0`.
+- `compileSdk` updated from `34` to `35`.
+- Java compatibility updated from `VERSION_1_8` to `VERSION_17`.
+- `namespace` set directly (removed conditional check).
+- Example app also updated: AGP `8.7.0`, Gradle `8.9`, Java `VERSION_17`.
+
+### 16. Bundle opus sources for reproducible builds
+
+Currently, Android fetches opus at build time via `FetchContent`, iOS/macOS use a build script that clones from GitHub, and Windows/Web use Docker. If GitHub is unavailable or the tag is removed, builds will fail.
+
+**Recommendation:**
+- Consider vendoring a source archive or using a git submodule for opus across all platforms.
+- Pin checksums for downloaded archives.
diff --git a/opus_dart/CHANGELOG.md b/opus_dart/CHANGELOG.md
new file mode 100644
index 0000000..44f04bb
--- /dev/null
+++ b/opus_dart/CHANGELOG.md
@@ -0,0 +1,84 @@
+## 3.0.5
+
+### Bug Fixes
+
+* Fix output buffer sizing for `BufferedOpusDecoder` (was 4x too small for float output) and inverted multiplier in `StreamOpusDecoder`
+* Fix `opus_encoder_ctl` variadic ABI mismatch under WASM by adding a non-variadic C wrapper
+* Prevent `asTypedList` view detachment on WASM memory growth by returning Dart-heap copies
+* Always copy streaming output to the Dart heap, eliminating use-after-write hazards in `StreamOpusEncoder` and `StreamOpusDecoder`
+* Guard encoder/decoder methods against use after `destroy()` with `OpusDestroyedError`
+* Attach `Finalizer` to encoder/decoder classes for GC-driven native resource cleanup
+* Prevent memory leak when a second native allocation throws
+* Add `_asString` bounds guard (cap at 256 bytes) to prevent unbounded scanning
+* Register missing `OpusCustomMode` opaque type in `init_web.dart` (was duplicating `OpusRepacketizer`)
+* Export `_opus_encoder_ctl` in WASM Dockerfile `EXPORTED_FUNCTIONS`
+
+### Refactoring
+
+* Extract duplicated encode logic into `_createOpusEncoder` and `_doEncode` / `_encodeBuffer` helpers
+* Extract duplicated decode logic into shared helpers and replace magic numbers with named constants
+* Deduplicate `OpusPacketUtils` with a shared `_withNativePacket` helper
+* Simplify `getOpusVersion` implementation
+* Add `bytesPerInt16Sample` and `bytesPerFloatSample` constants in `opus_dart_misc.dart`
+
+### Chores
+
+* Add `repository` field to pubspec and `CHANGELOG.md`
+* Fix typos across comments and documentation
+* Add RFC 6716 validation note to `maxDataBytes`
+* Add comprehensive tests for buffer sizing, bounds checking, use-after-destroy, and allocation failure cleanup
+
+
+## 3.0.4
+
+* Bump version
+
+
+## 3.0.3
+
+* Depend on newer `wasm_ffi` version for web support
+
+
+## 3.0.2
+
+* libopus 1.3.1
+
+
+## 3.0.1
+
+* libopus 1.3.1
+
+
+## 3.0.0
+
+* Migrate to `opus_flutter` namespace
+* Web support using [`wasm_ffi`](https://pub.dev/packages/wasm_ffi)
+* libopus 1.3.1
+
+
+## 2.0.1
+
+* libopus 1.3.1
+* Minor formatting fixes
+
+
+## 2.0.0
+
+* libopus 1.3.1
+* Null safety support
+
+
+## 1.0.4
+
+* libopus 1.3.1
+
+
+## 1.0.3
+
+* libopus 1.3.1
+
+
+## 1.0.0
+
+* libopus 1.3.1
+* Initial release
diff --git a/opus_dart/README.md b/opus_dart/README.md
index 7d7ac95..5d089c9 100644
--- a/opus_dart/README.md
+++ b/opus_dart/README.md
@@ -96,7 +96,7 @@ SimpleOpusEncoder createCbrEncoder() {
final encoder = SimpleOpusEncoder(
sampleRate: 8000,
channels: 1,
- application: Application.restrictedLowdely,
+ application: Application.restrictedLowdelay,
);
encoder.encoderCtl(request: OPUS_SET_VBR_REQUEST, value: 0);
encoder.encoderCtl(request: OPUS_SET_BITRATE_REQUEST, value: 15200);
diff --git a/opus_dart/analysis_options.yaml b/opus_dart/analysis_options.yaml
index ee359d9..8a9417d 100644
--- a/opus_dart/analysis_options.yaml
+++ b/opus_dart/analysis_options.yaml
@@ -1,6 +1,6 @@
-include: package:lints/recommended.yaml
-
-linter:
- rules:
- # Vendored FFI wrappers mirror C API naming (OPUS_SET_BITRATE_REQUEST, etc.)
- constant_identifier_names: false
+include: package:lints/recommended.yaml
+
+linter:
+ rules:
+ # Vendored FFI wrappers mirror C API naming (OPUS_SET_BITRATE_REQUEST, etc.)
+ constant_identifier_names: false
diff --git a/opus_dart/lib/src/init_web.dart b/opus_dart/lib/src/init_web.dart
index 164ce1c..67962c8 100644
--- a/opus_dart/lib/src/init_web.dart
+++ b/opus_dart/lib/src/init_web.dart
@@ -25,6 +25,6 @@ ApiObject createApiObject(Object lib) {
registerOpaqueType();
registerOpaqueType();
registerOpaqueType();
- registerOpaqueType();
+ registerOpaqueType();
return ApiObject(library, library.allocator);
}
diff --git a/opus_dart/lib/src/opus_dart_decoder.dart b/opus_dart/lib/src/opus_dart_decoder.dart
index f97738f..c883e92 100644
--- a/opus_dart/lib/src/opus_dart_decoder.dart
+++ b/opus_dart/lib/src/opus_dart_decoder.dart
@@ -7,6 +7,26 @@ import 'opus_dart_misc.dart';
int _packetDuration(int samples, int channels, int sampleRate) =>
((1000 * samples) ~/ (channels)) ~/ sampleRate;
+/// Allocates a temporary error pointer, calls `opus_decoder_create`, checks
+/// the result, and frees the error pointer. Returns the decoder on success or
+/// throws [OpusException] on failure.
+Pointer _createOpusDecoder({
+ required int sampleRate,
+ required int channels,
+}) {
+ final error = opus.allocator.call(1);
+ try {
+ final decoder =
+ opus.decoder.opus_decoder_create(sampleRate, channels, error);
+ if (error.value != opus_defines.OPUS_OK) {
+ throw OpusException(error.value);
+ }
+ return decoder;
+ } finally {
+ opus.allocator.free(error);
+ }
+}
+
/// Soft clips the [input] to a range from -1 to 1 and returns
/// the result.
///
@@ -18,24 +38,27 @@ int _packetDuration(int samples, int channels, int sampleRate) =>
/// method instead, since it avoids unnecessary memory copying.
Float32List pcmSoftClip({required Float32List input, required int channels}) {
Pointer nativePcm = opus.allocator.call(input.length);
- nativePcm.asTypedList(input.length).setAll(0, input);
- Pointer nativeBuffer = opus.allocator.call(channels);
+ Pointer? nativeBuffer;
try {
+ nativePcm.asTypedList(input.length).setAll(0, input);
+ nativeBuffer = opus.allocator.call(channels);
opus.decoder.opus_pcm_soft_clip(
nativePcm, input.length ~/ channels, channels, nativeBuffer);
return Float32List.fromList(nativePcm.asTypedList(input.length));
} finally {
+ if (nativeBuffer != null) opus.allocator.free(nativeBuffer);
opus.allocator.free(nativePcm);
- opus.allocator.free(nativeBuffer);
}
}
/// An easy to use implementation of [OpusDecoder].
/// Don't forget to call [destroy] once you are done with it.
///
-/// All method calls in this calls allocate their own memory everytime they are called.
+/// All method calls in this class allocate their own memory everytime they are called.
/// See the [BufferedOpusDecoder] for an implementation with less allocation calls.
class SimpleOpusDecoder extends OpusDecoder {
+ static final _finalizer = Finalizer((cleanup) => cleanup());
+
final Pointer _opusDecoder;
@override
final int sampleRate;
@@ -55,23 +78,61 @@ class SimpleOpusDecoder extends OpusDecoder {
SimpleOpusDecoder._(
this._opusDecoder, this.sampleRate, this.channels, this._softClipBuffer)
: _destroyed = false,
- _maxSamplesPerPacket = maxSamplesPerPacket(sampleRate, channels);
+ _maxSamplesPerPacket = maxSamplesPerPacket(sampleRate, channels) {
+ final decoder = _opusDecoder;
+ final softClip = _softClipBuffer;
+ _finalizer.attach(this, () {
+ opus.decoder.opus_decoder_destroy(decoder);
+ opus.allocator.free(softClip);
+ }, detach: this);
+ }
/// Creates an new [SimpleOpusDecoder] based on the [sampleRate] and [channels].
/// See the matching fields for more information about these parameters.
factory SimpleOpusDecoder({required int sampleRate, required int channels}) {
- Pointer error = opus.allocator.call(1);
- Pointer softClipBuffer = opus.allocator.call(channels);
- Pointer decoder =
- opus.decoder.opus_decoder_create(sampleRate, channels, error);
+ final softClipBuffer = opus.allocator.call(channels);
try {
- if (error.value != opus_defines.OPUS_OK) {
- opus.allocator.free(softClipBuffer);
- throw OpusException(error.value);
- }
+ final decoder =
+ _createOpusDecoder(sampleRate: sampleRate, channels: channels);
return SimpleOpusDecoder._(decoder, sampleRate, channels, softClipBuffer);
+ } catch (_) {
+ opus.allocator.free(softClipBuffer);
+ rethrow;
+ }
+ }
+
+ /// Allocates the input buffer if needed, computes frame size, invokes
+ /// [nativeDecode], checks the result, updates duration tracking, and frees
+ /// the input buffer. Returns the output sample count per channel.
+ ///
+ /// Callers are responsible for the destroyed check and for allocating/freeing
+ /// the output buffer in their own try/finally scope.
+ int _doDecode({
+ required Uint8List? input,
+ required bool fec,
+ required int? loss,
+ required int Function(Pointer inputPtr, int inputLen, int frameSize)
+ nativeDecode,
+ }) {
+ Pointer? inputNative;
+ try {
+ if (input != null) {
+ inputNative = opus.allocator.call(input.length);
+ inputNative.asTypedList(input.length).setAll(0, input);
+ }
+ final frameSize = (input == null || fec)
+ ? _estimateLoss(loss, lastPacketDurationMs)
+ : _maxSamplesPerPacket;
+ final outputSamplesPerChannel =
+ nativeDecode(inputNative ?? nullptr, input?.length ?? 0, frameSize);
+ if (outputSamplesPerChannel < opus_defines.OPUS_OK) {
+ throw OpusException(outputSamplesPerChannel);
+ }
+ _lastPacketDurationMs =
+ _packetDuration(outputSamplesPerChannel, channels, sampleRate);
+ return outputSamplesPerChannel;
} finally {
- opus.allocator.free(error);
+ if (inputNative != null) opus.allocator.free(inputNative);
}
}
@@ -87,41 +148,31 @@ class SimpleOpusDecoder extends OpusDecoder {
/// If you want to use forward error correction, don't report packet loss
/// by calling this method with `null` as input (unless it is a real packet
/// loss), but instead, wait for the next packet and call this method with
- /// the recieved packet, [fec] set to `true` and [loss] to the missing duration
+ /// the received packet, [fec] set to `true` and [loss] to the missing duration
/// of the missing audio in ms (as above). Then, call this method a second time with
/// the same packet, but with [fec] set to `false`. You can read more about the
/// correct usage of forward error correction [here](https://stackoverflow.com/questions/49427579/how-to-use-fec-feature-for-opus-codec).
- /// Note: A real packet loss occurse if you lose two or more packets in a row.
+ /// Note: A real packet loss occurs if you lose two or more packets in a row.
/// You are only able to restore the last lost packet and the other packets are
/// really lost. So for them, you have to report packet loss.
///
/// The input bytes need to represent a whole packet!
@override
Int16List decode({Uint8List? input, bool fec = false, int? loss}) {
- Pointer outputNative =
- opus.allocator.call(_maxSamplesPerPacket);
- Pointer inputNative;
- if (input != null) {
- inputNative = opus.allocator.call(input.length);
- inputNative.asTypedList(input.length).setAll(0, input);
- } else {
- inputNative = nullptr;
- }
- int frameSize = (input == null || fec)
- ? _estimateLoss(loss, lastPacketDurationMs)
- : _maxSamplesPerPacket;
- int outputSamplesPerChannel = opus.decoder.opus_decode(_opusDecoder,
- inputNative, input?.length ?? 0, outputNative, frameSize, fec ? 1 : 0);
+ if (_destroyed) throw OpusDestroyedError.decoder();
+ final outputNative = opus.allocator.call(_maxSamplesPerPacket);
try {
- if (outputSamplesPerChannel < opus_defines.OPUS_OK) {
- throw OpusException(outputSamplesPerChannel);
- }
- _lastPacketDurationMs =
- _packetDuration(outputSamplesPerChannel, channels, sampleRate);
+ final outputSamplesPerChannel = _doDecode(
+ input: input,
+ fec: fec,
+ loss: loss,
+ nativeDecode: (inputPtr, inputLen, frameSize) => opus.decoder
+ .opus_decode(_opusDecoder, inputPtr, inputLen, outputNative,
+ frameSize, fec ? 1 : 0),
+ );
return Int16List.fromList(
outputNative.asTypedList(outputSamplesPerChannel * channels));
} finally {
- opus.allocator.free(inputNative);
opus.allocator.free(outputNative);
}
}
@@ -141,26 +192,17 @@ class SimpleOpusDecoder extends OpusDecoder {
bool fec = false,
bool autoSoftClip = false,
int? loss}) {
- Pointer outputNative =
- opus.allocator.call(_maxSamplesPerPacket);
- Pointer inputNative;
- if (input != null) {
- inputNative = opus.allocator.call(input.length);
- inputNative.asTypedList(input.length).setAll(0, input);
- } else {
- inputNative = nullptr;
- }
- int frameSize = (input == null || fec)
- ? _estimateLoss(loss, lastPacketDurationMs)
- : _maxSamplesPerPacket;
- int outputSamplesPerChannel = opus.decoder.opus_decode_float(_opusDecoder,
- inputNative, input?.length ?? 0, outputNative, frameSize, fec ? 1 : 0);
+ if (_destroyed) throw OpusDestroyedError.decoder();
+ final outputNative = opus.allocator.call(_maxSamplesPerPacket);
try {
- if (outputSamplesPerChannel < opus_defines.OPUS_OK) {
- throw OpusException(outputSamplesPerChannel);
- }
- _lastPacketDurationMs =
- _packetDuration(outputSamplesPerChannel, channels, sampleRate);
+ final outputSamplesPerChannel = _doDecode(
+ input: input,
+ fec: fec,
+ loss: loss,
+ nativeDecode: (inputPtr, inputLen, frameSize) => opus.decoder
+ .opus_decode_float(_opusDecoder, inputPtr, inputLen, outputNative,
+ frameSize, fec ? 1 : 0),
+ );
if (autoSoftClip) {
opus.decoder.opus_pcm_soft_clip(outputNative,
outputSamplesPerChannel ~/ channels, channels, _softClipBuffer);
@@ -168,7 +210,6 @@ class SimpleOpusDecoder extends OpusDecoder {
return Float32List.fromList(
outputNative.asTypedList(outputSamplesPerChannel * channels));
} finally {
- opus.allocator.free(inputNative);
opus.allocator.free(outputNative);
}
}
@@ -179,6 +220,7 @@ class SimpleOpusDecoder extends OpusDecoder {
_destroyed = true;
opus.decoder.opus_decoder_destroy(_opusDecoder);
opus.allocator.free(_softClipBuffer);
+ _finalizer.detach(this);
}
}
}
@@ -188,9 +230,9 @@ class SimpleOpusDecoder extends OpusDecoder {
///
/// The idea behind this implementation is to reduce the amount of memory allocation calls.
/// Instead of allocating new buffers everytime something is decoded, the buffers are
-/// allocated at initalization. Then, an opus packet is directly written into the [inputBuffer],
+/// allocated at initialization. Then, an opus packet is directly written into the [inputBuffer],
/// the [inputBufferIndex] is updated, based on how many bytes where written, and
-/// one of the deocde methods is called. The decoded pcm samples can then be accessed using
+/// one of the decode methods is called. The decoded pcm samples can then be accessed using
/// the [outputBuffer] getter (or one of the [outputBufferAsInt16List] or [outputBufferAsFloat32List] convenience getters).
/// ```
/// BufferedOpusDecoder decoder;
@@ -210,6 +252,8 @@ class SimpleOpusDecoder extends OpusDecoder {
/// }
/// ```
class BufferedOpusDecoder extends OpusDecoder {
+ static final _finalizer = Finalizer((cleanup) => cleanup());
+
final Pointer _opusDecoder;
@override
final int sampleRate;
@@ -223,7 +267,7 @@ class BufferedOpusDecoder extends OpusDecoder {
int? _lastPacketDurationMs;
/// The size of the allocated the input buffer in bytes.
- /// Should be choosen big enough to hold a maximal opus packet
+ /// Should be chosen big enough to hold a maximal opus packet
/// with size of [maxDataBytes] bytes.
final int maxInputBufferSizeBytes;
@@ -241,8 +285,8 @@ class BufferedOpusDecoder extends OpusDecoder {
Uint8List get inputBuffer =>
_inputBuffer.asTypedList(maxInputBufferSizeBytes);
- /// The size of the allocated the output buffer. If this value is choosen
- /// to small, this decoder will not be capable of decoding some packets.
+ /// The size of the allocated the output buffer. If this value is chosen
+ /// too small, this decoder will not be capable of decoding some packets.
///
/// See the constructor for information, how to choose this.
final int maxOutputBufferSizeBytes;
@@ -253,17 +297,24 @@ class BufferedOpusDecoder extends OpusDecoder {
/// The data are pcm samples, either encoded as s16le or floats, depending on
/// what method was used to decode the input packet.
///
- /// This method does not copy data from native memory to dart memory but
- /// rather gives a view backed by native memory.
- Uint8List get outputBuffer => _outputBuffer.asTypedList(_outputBufferIndex);
+ /// Returns a copy of the native output buffer. This is safe across WASM
+ /// memory growth — the returned list remains valid even if subsequent
+ /// allocations replace the underlying ArrayBuffer.
+ Uint8List get outputBuffer =>
+ Uint8List.fromList(_outputBuffer.asTypedList(_outputBufferIndex));
/// Convenience method to get the current output buffer as s16le.
- Int16List get outputBufferAsInt16List =>
- _outputBuffer.cast().asTypedList(_outputBufferIndex ~/ 2);
+ /// Returns a copy safe across WASM memory growth.
+ Int16List get outputBufferAsInt16List => Int16List.fromList(_outputBuffer
+ .cast()
+ .asTypedList(_outputBufferIndex ~/ bytesPerInt16Sample));
/// Convenience method to get the current output buffer as floats.
+ /// Returns a copy safe across WASM memory growth.
Float32List get outputBufferAsFloat32List =>
- _outputBuffer.cast().asTypedList(_outputBufferIndex ~/ 4);
+ Float32List.fromList(_outputBuffer
+ .cast()
+ .asTypedList(_outputBufferIndex ~/ bytesPerFloatSample));
final Pointer _softClipBuffer;
@@ -278,17 +329,28 @@ class BufferedOpusDecoder extends OpusDecoder {
this._softClipBuffer)
: _destroyed = false,
inputBufferIndex = 0,
- _outputBufferIndex = 0;
+ _outputBufferIndex = 0 {
+ final decoder = _opusDecoder;
+ final input = _inputBuffer;
+ final output = _outputBuffer;
+ final softClip = _softClipBuffer;
+ _finalizer.attach(this, () {
+ opus.decoder.opus_decoder_destroy(decoder);
+ opus.allocator.free(input);
+ opus.allocator.free(output);
+ opus.allocator.free(softClip);
+ }, detach: this);
+ }
/// Creates an new [BufferedOpusDecoder] based on the [sampleRate] and [channels].
/// The native allocated buffer size is determined by [maxInputBufferSizeBytes] and [maxOutputBufferSizeBytes].
///
/// You should choose [maxInputBufferSizeBytes] big enough to put every opus packet you want to decode in it.
- /// If you omit this parameter, [maxDataByes] is used, which guarantees that there is enough space for every
+ /// If you omit this parameter, [maxDataBytes] is used, which guarantees that there is enough space for every
/// valid opus packet.
///
/// [maxOutputBufferSizeBytes] is the size of the output buffer, which will hold the decoded frames.
- /// If this value is choosen to small, this decoder will not be capable of decoding some packets.
+ /// If this value is chosen too small, this decoder will not be capable of decoding some packets.
/// If you are unsure, just let it `null`, so the maximum size of resulting frames will be calculated
/// Here is some more theory about that:
/// A single opus packet may contain up to 120ms of audio, so assuming you are decoding
@@ -306,23 +368,18 @@ class BufferedOpusDecoder extends OpusDecoder {
int? maxInputBufferSizeBytes,
int? maxOutputBufferSizeBytes}) {
maxInputBufferSizeBytes ??= maxDataBytes;
- maxOutputBufferSizeBytes ??= maxSamplesPerPacket(sampleRate, channels);
- Pointer error = opus.allocator.call(1);
- Pointer input = opus.allocator.call(maxInputBufferSizeBytes);
- Pointer output =
- opus.allocator.call(maxOutputBufferSizeBytes);
- Pointer softClipBuffer = opus.allocator.call(channels);
- Pointer encoder =
- opus.decoder.opus_decoder_create(sampleRate, channels, error);
+ maxOutputBufferSizeBytes ??=
+ bytesPerFloatSample * maxSamplesPerPacket(sampleRate, channels);
+ final input = opus.allocator.call(maxInputBufferSizeBytes);
+ Pointer? output;
+ Pointer? softClipBuffer;
try {
- if (error.value != opus_defines.OPUS_OK) {
- opus.allocator.free(input);
- opus.allocator.free(output);
- opus.allocator.free(softClipBuffer);
- throw OpusException(error.value);
- }
+ output = opus.allocator.call(maxOutputBufferSizeBytes);
+ softClipBuffer = opus.allocator.call(channels);
+ final decoder =
+ _createOpusDecoder(sampleRate: sampleRate, channels: channels);
return BufferedOpusDecoder._(
- encoder,
+ decoder,
sampleRate,
channels,
input,
@@ -330,11 +387,48 @@ class BufferedOpusDecoder extends OpusDecoder {
output,
maxOutputBufferSizeBytes,
softClipBuffer);
- } finally {
- opus.allocator.free(error);
+ } catch (_) {
+ if (softClipBuffer != null) opus.allocator.free(softClipBuffer);
+ if (output != null) opus.allocator.free(output);
+ opus.allocator.free(input);
+ rethrow;
}
}
+ /// Computes the input pointer and frame size from [inputBufferIndex],
+ /// invokes the appropriate native decode function, checks the result,
+ /// and updates duration tracking and output buffer index.
+ void _decodeBuffer(
+ {required bool useFloat, required bool fec, required int? loss}) {
+ if (_destroyed) throw OpusDestroyedError.decoder();
+ final bytesPerSample = useFloat ? bytesPerFloatSample : bytesPerInt16Sample;
+ Pointer inputNative;
+ int frameSize;
+ if (inputBufferIndex > 0) {
+ inputNative = _inputBuffer;
+ frameSize = maxOutputBufferSizeBytes ~/ (bytesPerSample * channels);
+ } else {
+ inputNative = nullptr;
+ frameSize = _estimateLoss(loss, lastPacketDurationMs);
+ }
+ final outputSamplesPerChannel = useFloat
+ ? opus.decoder.opus_decode_float(
+ _opusDecoder,
+ inputNative,
+ inputBufferIndex,
+ _outputBuffer.cast(),
+ frameSize,
+ fec ? 1 : 0)
+ : opus.decoder.opus_decode(_opusDecoder, inputNative, inputBufferIndex,
+ _outputBuffer.cast(), frameSize, fec ? 1 : 0);
+ if (outputSamplesPerChannel < opus_defines.OPUS_OK) {
+ throw OpusException(outputSamplesPerChannel);
+ }
+ _lastPacketDurationMs =
+ _packetDuration(outputSamplesPerChannel, channels, sampleRate);
+ _outputBufferIndex = bytesPerSample * outputSamplesPerChannel * channels;
+ }
+
/// Interpretes [inputBufferIndex] bytes from the [inputBuffer] as a whole
/// opus packet and decodes them to s16le samples, stored in the [outputBuffer].
/// Set [inputBufferIndex] to `0` to indicate packet loss.
@@ -353,7 +447,7 @@ class BufferedOpusDecoder extends OpusDecoder {
/// in ms (as above). Then, call this method a second time with
/// the same packet, but with [fec] set to `false`. You can read more about the
/// correct usage of forward error correction [here](https://stackoverflow.com/questions/49427579/how-to-use-fec-feature-for-opus-codec).
- /// Note: A real packet loss occurse if you lose two or more packets in a row.
+ /// Note: A real packet loss occurs if you lose two or more packets in a row.
/// You are only able to restore the last lost packet and the other packets are
/// really lost. So for them, you have to report packet loss.
///
@@ -362,28 +456,7 @@ class BufferedOpusDecoder extends OpusDecoder {
/// The returned list is actually just the [outputBufferAsInt16List].
@override
Int16List decode({bool fec = false, int? loss}) {
- Pointer inputNative;
- int frameSize;
- if (inputBufferIndex > 0) {
- inputNative = _inputBuffer;
- frameSize = maxOutputBufferSizeBytes ~/ (2 * channels);
- } else {
- inputNative = nullptr;
- frameSize = _estimateLoss(loss, lastPacketDurationMs);
- }
- int outputSamplesPerChannel = opus.decoder.opus_decode(
- _opusDecoder,
- inputNative,
- inputBufferIndex,
- _outputBuffer.cast(),
- frameSize,
- fec ? 1 : 0);
- if (outputSamplesPerChannel < opus_defines.OPUS_OK) {
- throw OpusException(outputSamplesPerChannel);
- }
- _lastPacketDurationMs =
- _packetDuration(outputSamplesPerChannel, channels, sampleRate);
- _outputBufferIndex = 2 * outputSamplesPerChannel * channels;
+ _decodeBuffer(useFloat: false, fec: fec, loss: loss);
return outputBufferAsInt16List;
}
@@ -391,34 +464,13 @@ class BufferedOpusDecoder extends OpusDecoder {
/// opus packet and decodes them to float samples, stored in the [outputBuffer].
/// Set [inputBufferIndex] to `0` to indicate packet loss.
///
- /// If [autoSoftClip] is true, this decoders [pcmSoftClipOutputBuffer] method is automatically called.
+ /// If [autoSoftClip] is true, this decoder's [pcmSoftClipOutputBuffer] method is automatically called.
///
/// Apart from that, this method behaves just as [decode], so see there for more information.
@override
Float32List decodeFloat(
{bool autoSoftClip = false, bool fec = false, int? loss}) {
- Pointer inputNative;
- int frameSize;
- if (inputBufferIndex > 0) {
- inputNative = _inputBuffer;
- frameSize = maxOutputBufferSizeBytes ~/ (4 * channels);
- } else {
- inputNative = nullptr;
- frameSize = _estimateLoss(loss, lastPacketDurationMs);
- }
- int outputSamplesPerChannel = opus.decoder.opus_decode_float(
- _opusDecoder,
- inputNative,
- inputBufferIndex,
- _outputBuffer.cast(),
- frameSize,
- fec ? 1 : 0);
- if (outputSamplesPerChannel < opus_defines.OPUS_OK) {
- throw OpusException(outputSamplesPerChannel);
- }
- _lastPacketDurationMs =
- _packetDuration(outputSamplesPerChannel, channels, sampleRate);
- _outputBufferIndex = 4 * outputSamplesPerChannel * channels;
+ _decodeBuffer(useFloat: true, fec: fec, loss: loss);
if (autoSoftClip) {
return pcmSoftClipOutputBuffer();
}
@@ -433,6 +485,7 @@ class BufferedOpusDecoder extends OpusDecoder {
opus.allocator.free(_inputBuffer);
opus.allocator.free(_outputBuffer);
opus.allocator.free(_softClipBuffer);
+ _finalizer.detach(this);
}
}
@@ -440,8 +493,12 @@ class BufferedOpusDecoder extends OpusDecoder {
///
/// Behaves like the toplevel [pcmSoftClip] function, but without unnecessary copying.
Float32List pcmSoftClipOutputBuffer() {
- opus.decoder.opus_pcm_soft_clip(_outputBuffer.cast(),
- _outputBufferIndex ~/ (4 * channels), channels, _softClipBuffer);
+ if (_destroyed) throw OpusDestroyedError.decoder();
+ opus.decoder.opus_pcm_soft_clip(
+ _outputBuffer.cast(),
+ _outputBufferIndex ~/ (bytesPerFloatSample * channels),
+ channels,
+ _softClipBuffer);
return outputBufferAsFloat32List;
}
}
@@ -455,7 +512,7 @@ abstract class OpusDecoder {
/// Number of channels, must be 1 for mono or 2 for stereo.
int get channels;
- /// Wheter this decoder was already destroyed by calling [destroy].
+ /// Whether this decoder was already destroyed by calling [destroy].
/// If so, calling any method will result in an [OpusDestroyedError].
bool get destroyed;
@@ -476,5 +533,5 @@ int _estimateLoss(int? loss, int? lastPacketDurationMs) {
throw StateError(
'Tried to estimate the loss based on the last packets duration, but there was no last packet!\n'
'This happend because you called a decode function with no input (null as input in SimpleOpusDecoder or 0 as inputBufferIndex in BufferedOpusDecoder), but failed to specify how many milliseconds were lost.\n'
- 'And since there was no previous sucessfull decoded packet, the decoder could not estimate how many milliseconds are missing.');
+ 'And since there was no previous successful decoded packet, the decoder could not estimate how many milliseconds are missing.');
}
diff --git a/opus_dart/lib/src/opus_dart_encoder.dart b/opus_dart/lib/src/opus_dart_encoder.dart
index 0c854c9..8fe7c47 100644
--- a/opus_dart/lib/src/opus_dart_encoder.dart
+++ b/opus_dart/lib/src/opus_dart_encoder.dart
@@ -4,12 +4,35 @@ import '../wrappers/opus_encoder.dart' as opus_encoder;
import '../wrappers/opus_defines.dart' as opus_defines;
import 'opus_dart_misc.dart';
+/// Allocates a temporary error pointer, calls `opus_encoder_create`, checks
+/// the result, and frees the error pointer. Returns the encoder on success or
+/// throws [OpusException] on failure.
+Pointer _createOpusEncoder({
+ required int sampleRate,
+ required int channels,
+ required Application application,
+}) {
+ final error = opus.allocator.call(1);
+ try {
+ final encoder = opus.encoder.opus_encoder_create(
+ sampleRate, channels, _applicationCodes[application]!, error);
+ if (error.value != opus_defines.OPUS_OK) {
+ throw OpusException(error.value);
+ }
+ return encoder;
+ } finally {
+ opus.allocator.free(error);
+ }
+}
+
/// An easy to use implementation of [OpusEncoder].
/// Don't forget to call [destroy] once you are done with it.
///
-/// All method calls in this calls allocate their own memory everytime they are called.
+/// All method calls in this class allocate their own memory everytime they are called.
/// See the [BufferedOpusEncoder] for an implementation with less allocation calls.
class SimpleOpusEncoder extends OpusEncoder {
+ static final _finalizer = Finalizer((cleanup) => cleanup());
+
final Pointer _opusEncoder;
@override
final int sampleRate;
@@ -23,7 +46,12 @@ class SimpleOpusEncoder extends OpusEncoder {
SimpleOpusEncoder._(
this._opusEncoder, this.sampleRate, this.channels, this.application)
- : _destroyed = false;
+ : _destroyed = false {
+ final encoder = _opusEncoder;
+ _finalizer.attach(this, () {
+ opus.encoder.opus_encoder_destroy(encoder);
+ }, detach: this);
+ }
/// Creates an new [SimpleOpusEncoder] based on the [sampleRate], [channels] and [application] type.
/// See the matching fields for more information about these parameters.
@@ -31,17 +59,33 @@ class SimpleOpusEncoder extends OpusEncoder {
{required int sampleRate,
required int channels,
required Application application}) {
- Pointer error = opus.allocator.call(1);
- Pointer encoder = opus.encoder
- .opus_encoder_create(
- sampleRate, channels, _applicationCodes[application]!, error);
+ final encoder = _createOpusEncoder(
+ sampleRate: sampleRate, channels: channels, application: application);
+ return SimpleOpusEncoder._(encoder, sampleRate, channels, application);
+ }
+
+ /// Allocates the output buffer, computes the per-channel sample count,
+ /// invokes [nativeEncode], checks the result, and returns a Dart-heap copy
+ /// of the encoded opus packet. The output buffer is always freed.
+ ///
+ /// Callers are responsible for allocating and freeing the input buffer in
+ /// their own try/finally scope.
+ Uint8List _doEncode({
+ required int inputSampleCount,
+ required int maxOutputSizeBytes,
+ required int Function(int sampleCountPerChannel, Pointer output)
+ nativeEncode,
+ }) {
+ final outputNative = opus.allocator.call(maxOutputSizeBytes);
try {
- if (error.value != opus_defines.OPUS_OK) {
- throw OpusException(error.value);
+ final sampleCountPerChannel = inputSampleCount ~/ channels;
+ final outputLength = nativeEncode(sampleCountPerChannel, outputNative);
+ if (outputLength < opus_defines.OPUS_OK) {
+ throw OpusException(outputLength);
}
- return SimpleOpusEncoder._(encoder, sampleRate, channels, application);
+ return Uint8List.fromList(outputNative.asTypedList(outputLength));
} finally {
- opus.allocator.free(error);
+ opus.allocator.free(outputNative);
}
}
@@ -54,29 +98,26 @@ class SimpleOpusEncoder extends OpusEncoder {
/// `input.length = 2 * 48000Hz * 0.02s = 1920`.
///
/// [maxOutputSizeBytes] is used to allocate the output buffer. It can be used to impose an instant
- /// upper limit on the bitrate, but must not be to small to hold the encoded data (or an exception will be thrown).
+ /// upper limit on the bitrate, but must not be too small to hold the encoded data (or an exception will be thrown).
/// The default value of [maxDataBytes] ensures that there is enough space.
///
- /// The returnes list contains the bytes of the encoded opus packet.
+ /// The returned list contains the bytes of the encoded opus packet.
///
/// [input] and the returned list are copied to and respectively from native memory.
Uint8List encode(
{required Int16List input, int maxOutputSizeBytes = maxDataBytes}) {
- Pointer inputNative = opus.allocator.call(input.length);
- inputNative.asTypedList(input.length).setAll(0, input);
- Pointer outputNative =
- opus.allocator.call(maxOutputSizeBytes);
- int sampleCountPerChannel = input.length ~/ channels;
- int outputLength = opus.encoder.opus_encode(_opusEncoder, inputNative,
- sampleCountPerChannel, outputNative, maxOutputSizeBytes);
+ if (_destroyed) throw OpusDestroyedError.encoder();
+ final inputNative = opus.allocator.call(input.length);
try {
- if (outputLength < opus_defines.OPUS_OK) {
- throw OpusException(outputLength);
- }
- return Uint8List.fromList(outputNative.asTypedList(outputLength));
+ inputNative.asTypedList(input.length).setAll(0, input);
+ return _doEncode(
+ inputSampleCount: input.length,
+ maxOutputSizeBytes: maxOutputSizeBytes,
+ nativeEncode: (spc, output) => opus.encoder.opus_encode(
+ _opusEncoder, inputNative, spc, output, maxOutputSizeBytes),
+ );
} finally {
opus.allocator.free(inputNative);
- opus.allocator.free(outputNative);
}
}
@@ -85,21 +126,18 @@ class SimpleOpusEncoder extends OpusEncoder {
/// This method behaves just as [encode], so see there for more information.
Uint8List encodeFloat(
{required Float32List input, int maxOutputSizeBytes = maxDataBytes}) {
- Pointer inputNative = opus.allocator.call(input.length);
- inputNative.asTypedList(input.length).setAll(0, input);
- Pointer outputNative =
- opus.allocator.call(maxOutputSizeBytes);
- int sampleCountPerChannel = input.length ~/ channels;
- int outputLength = opus.encoder.opus_encode_float(_opusEncoder, inputNative,
- sampleCountPerChannel, outputNative, maxOutputSizeBytes);
+ if (_destroyed) throw OpusDestroyedError.encoder();
+ final inputNative = opus.allocator.call(input.length);
try {
- if (outputLength < opus_defines.OPUS_OK) {
- throw OpusException(outputLength);
- }
- return Uint8List.fromList(outputNative.asTypedList(outputLength));
+ inputNative.asTypedList(input.length).setAll(0, input);
+ return _doEncode(
+ inputSampleCount: input.length,
+ maxOutputSizeBytes: maxOutputSizeBytes,
+ nativeEncode: (spc, output) => opus.encoder.opus_encode_float(
+ _opusEncoder, inputNative, spc, output, maxOutputSizeBytes),
+ );
} finally {
opus.allocator.free(inputNative);
- opus.allocator.free(outputNative);
}
}
@@ -108,6 +146,7 @@ class SimpleOpusEncoder extends OpusEncoder {
if (!_destroyed) {
_destroyed = true;
opus.encoder.opus_encoder_destroy(_opusEncoder);
+ _finalizer.detach(this);
}
}
}
@@ -117,7 +156,7 @@ class SimpleOpusEncoder extends OpusEncoder {
///
/// The idea behind this implementation is to reduce the amount of memory allocation calls.
/// Instead of allocating new buffers everytime something is encoded, the buffers are
-/// allocated at initalization. Then, pcm samples is directly written into the [inputBuffer],
+/// allocated at initialization. Then, pcm samples is directly written into the [inputBuffer],
/// the [inputBufferIndex] is updated, based on how many data where written, and
/// one of the encode methods is called. The encoded opus packet can then be accessed using
/// the [outputBuffer] getter.
@@ -142,6 +181,8 @@ class SimpleOpusEncoder extends OpusEncoder {
/// }
/// ```
class BufferedOpusEncoder extends OpusEncoder {
+ static final _finalizer = Finalizer((cleanup) => cleanup());
+
final Pointer _opusEncoder;
@override
final int sampleRate;
@@ -154,7 +195,7 @@ class BufferedOpusEncoder extends OpusEncoder {
@override
bool get destroyed => _destroyed;
- /// The size of the allocated the input buffer in bytes (not sampels).
+ /// The size of the allocated the input buffer in bytes (not samples).
final int maxInputBufferSizeBytes;
/// Indicates, how many bytes of data are currently stored in the [inputBuffer].
@@ -172,8 +213,8 @@ class BufferedOpusEncoder extends OpusEncoder {
_inputBuffer.asTypedList(maxInputBufferSizeBytes);
/// The size of the allocated the output buffer. It can be used to impose an instant
- /// upper limit on the bitrate, but must not be to small to hold the encoded data.
- /// Otherwise, the enocde methods might throw an exception.
+ /// upper limit on the bitrate, but must not be too small to hold the encoded data.
+ /// Otherwise, the encode methods might throw an exception.
/// The default value of [maxDataBytes] ensures that there is enough space.
final int maxOutputBufferSizeBytes;
int _outputBufferIndex;
@@ -182,9 +223,11 @@ class BufferedOpusEncoder extends OpusEncoder {
/// The portion of the allocated output buffer that is currently filled with data.
/// The data represents an opus packet in bytes.
///
- /// This method does not copy data from native memory to dart memory but
- /// rather gives a view backed by native memory.
- Uint8List get outputBuffer => _outputBuffer.asTypedList(_outputBufferIndex);
+ /// Returns a copy of the native output buffer. This is safe across WASM
+ /// memory growth — the returned list remains valid even if subsequent
+ /// allocations replace the underlying ArrayBuffer.
+ Uint8List get outputBuffer =>
+ Uint8List.fromList(_outputBuffer.asTypedList(_outputBufferIndex));
BufferedOpusEncoder._(
this._opusEncoder,
@@ -197,17 +240,26 @@ class BufferedOpusEncoder extends OpusEncoder {
this.maxOutputBufferSizeBytes)
: _destroyed = false,
inputBufferIndex = 0,
- _outputBufferIndex = 0;
+ _outputBufferIndex = 0 {
+ final encoder = _opusEncoder;
+ final input = _inputBuffer;
+ final output = _outputBuffer;
+ _finalizer.attach(this, () {
+ opus.encoder.opus_encoder_destroy(encoder);
+ opus.allocator.free(input);
+ opus.allocator.free(output);
+ }, detach: this);
+ }
/// Creates an new [BufferedOpusEncoder] based on the [sampleRate], [channels] and [application] type.
/// The native allocated buffer size is determined by [maxInputBufferSizeBytes] and [maxOutputBufferSizeBytes].
///
- /// If [maxInputBufferSizeBytes] is omitted, it is callculated as 4 * [maxSamplesPerPacket].
+ /// If [maxInputBufferSizeBytes] is omitted, it is calculated as [bytesPerFloatSample] * [maxSamplesPerPacket].
/// This ensures that the input buffer is big enough to hold the largest possible
- /// frame (120ms at 48000Hz) in float (=4 byte) representation.
- /// If you know that you only use input data in s16le representation you can manually set this to 2 * [maxSamplesPerPacket].
+ /// frame (120ms at 48000Hz) in float representation.
+ /// If you know that you only use input data in s16le representation you can manually set this to [bytesPerInt16Sample] * [maxSamplesPerPacket].
///
- /// [maxOutputBufferSizeBytes] defaults to [maxDataBytes] to guarantee that their is enough space in the
+ /// [maxOutputBufferSizeBytes] defaults to [maxDataBytes] to guarantee that there is enough space in the
/// output buffer for any possible valid input.
///
/// For the other parameters, see the matching fields for more information.
@@ -217,32 +269,52 @@ class BufferedOpusEncoder extends OpusEncoder {
required Application application,
int? maxInputBufferSizeBytes,
int? maxOutputBufferSizeBytes}) {
- maxInputBufferSizeBytes ??= 4 * maxSamplesPerPacket(sampleRate, channels);
+ maxInputBufferSizeBytes ??=
+ bytesPerFloatSample * maxSamplesPerPacket(sampleRate, channels);
maxOutputBufferSizeBytes ??= maxDataBytes;
- Pointer error = opus.allocator.call(1);
- Pointer input = opus.allocator.call(maxInputBufferSizeBytes);
- Pointer output =
- opus.allocator.call(maxOutputBufferSizeBytes);
- Pointer encoder = opus.encoder
- .opus_encoder_create(
- sampleRate, channels, _applicationCodes[application]!, error);
+ final input = opus.allocator.call(maxInputBufferSizeBytes);
+ Pointer? output;
try {
- if (error.value != opus_defines.OPUS_OK) {
- opus.allocator.free(input);
- opus.allocator.free(output);
- throw OpusException(error.value);
- }
+ output = opus.allocator.call(maxOutputBufferSizeBytes);
+ final encoder = _createOpusEncoder(
+ sampleRate: sampleRate, channels: channels, application: application);
return BufferedOpusEncoder._(encoder, sampleRate, channels, application,
input, maxInputBufferSizeBytes, output, maxOutputBufferSizeBytes);
- } finally {
- opus.allocator.free(error);
+ } catch (_) {
+ if (output != null) opus.allocator.free(output);
+ opus.allocator.free(input);
+ rethrow;
}
}
int encoderCtl({required int request, required int value}) {
+ if (_destroyed) throw OpusDestroyedError.encoder();
return opus.encoder.opus_encoder_ctl(_opusEncoder, request, value);
}
+ /// Computes the per-channel sample count from [inputBufferIndex], invokes
+ /// the appropriate native encode function, checks the result, and returns
+ /// [outputBuffer].
+ Uint8List _encodeBuffer({required bool useFloat}) {
+ if (_destroyed) throw OpusDestroyedError.encoder();
+ final bytesPerSample = useFloat ? bytesPerFloatSample : bytesPerInt16Sample;
+ final sampleCountPerChannel =
+ inputBufferIndex ~/ (channels * bytesPerSample);
+ _outputBufferIndex = useFloat
+ ? opus.encoder.opus_encode_float(
+ _opusEncoder,
+ _inputBuffer.cast(),
+ sampleCountPerChannel,
+ _outputBuffer,
+ maxOutputBufferSizeBytes)
+ : opus.encoder.opus_encode(_opusEncoder, _inputBuffer.cast(),
+ sampleCountPerChannel, _outputBuffer, maxOutputBufferSizeBytes);
+ if (_outputBufferIndex < opus_defines.OPUS_OK) {
+ throw OpusException(_outputBufferIndex);
+ }
+ return outputBuffer;
+ }
+
/// Interpets [inputBufferIndex] bytes of the [inputBuffer] as s16le pcm data, and encodes them to the [outputBuffer].
/// This means, that this method encodes `[inputBufferIndex]/2` samples, since `inputBufferIndex` is in bytes,
/// and s16le uses two bytes per sample.
@@ -254,39 +326,15 @@ class BufferedOpusEncoder extends OpusEncoder {
/// `sampleCount = 2 * 48000Hz * 0.02s = 1920`.
///
/// The returned list is actually just the [outputBuffer].
- Uint8List encode() {
- int sampleCountPerChannel = inputBufferIndex ~/ (channels * 2);
- _outputBufferIndex = opus.encoder.opus_encode(
- _opusEncoder,
- _inputBuffer.cast(),
- sampleCountPerChannel,
- _outputBuffer,
- maxOutputBufferSizeBytes);
- if (_outputBufferIndex < opus_defines.OPUS_OK) {
- throw OpusException(_outputBufferIndex);
- }
- return outputBuffer;
- }
+ Uint8List encode() => _encodeBuffer(useFloat: false);
/// Interpets [inputBufferIndex] bytes of the [inputBuffer] as float pcm data, and encodes them to the [outputBuffer].
/// This means, that this method encodes `[inputBufferIndex]/4` samples, since `inputBufferIndex` is in bytes,
- /// and the float represntation uses two bytes per sample.
+ /// and the float representation uses four bytes per sample.
///
/// Except that the sample count is calculated by dividing the [inputBufferIndex] by 4 and not by 2,
/// this method behaves just as [encode], so see there for more information.
- Uint8List encodeFloat() {
- int sampleCountPerChannel = inputBufferIndex ~/ (channels * 4);
- _outputBufferIndex = opus.encoder.opus_encode_float(
- _opusEncoder,
- _inputBuffer.cast(),
- sampleCountPerChannel,
- _outputBuffer,
- maxOutputBufferSizeBytes);
- if (_outputBufferIndex < opus_defines.OPUS_OK) {
- throw OpusException(_outputBufferIndex);
- }
- return outputBuffer;
- }
+ Uint8List encodeFloat() => _encodeBuffer(useFloat: true);
@override
void destroy() {
@@ -295,6 +343,7 @@ class BufferedOpusEncoder extends OpusEncoder {
opus.encoder.opus_encoder_destroy(_opusEncoder);
opus.allocator.free(_inputBuffer);
opus.allocator.free(_outputBuffer);
+ _finalizer.detach(this);
}
}
}
@@ -312,7 +361,7 @@ abstract class OpusEncoder {
/// Setting the right application type can increase quality of the encoded frames.
Application get application;
- /// Wheter this encoder was already destroyed by calling [destroy].
+ /// Whether this encoder was already destroyed by calling [destroy].
/// If so, calling any method will result in an [OpusDestroyedError].
bool get destroyed;
@@ -321,13 +370,13 @@ abstract class OpusEncoder {
void destroy();
}
-/// Represents the different apllication types an [OpusEncoder] supports.
-/// Setting the right apllication type when creating an encoder can improve quality.
-enum Application { voip, audio, restrictedLowdely }
+/// Represents the different application types an [OpusEncoder] supports.
+/// Setting the right application type when creating an encoder can improve quality.
+enum Application { voip, audio, restrictedLowdelay }
const Map _applicationCodes = {
Application.voip: opus_defines.OPUS_APPLICATION_VOIP,
Application.audio: opus_defines.OPUS_APPLICATION_AUDIO,
- Application.restrictedLowdely:
+ Application.restrictedLowdelay:
opus_defines.OPUS_APPLICATION_RESTRICTED_LOWDELAY
};
diff --git a/opus_dart/lib/src/opus_dart_misc.dart b/opus_dart/lib/src/opus_dart_misc.dart
index 5b8ae2f..34735f5 100644
--- a/opus_dart/lib/src/opus_dart_misc.dart
+++ b/opus_dart/lib/src/opus_dart_misc.dart
@@ -6,28 +6,41 @@ import '../wrappers/opus_libinfo.dart' as opus_libinfo;
import '../wrappers/opus_encoder.dart' as opus_encoder;
import '../wrappers/opus_decoder.dart' as opus_decoder;
+/// Byte width of a single s16le PCM sample (Int16).
+const int bytesPerInt16Sample = 2;
+
+/// Byte width of a single float PCM sample (Float32).
+const int bytesPerFloatSample = 4;
+
/// Max bitstream size of a single opus packet.
///
/// See [here](https://stackoverflow.com/questions/55698317/what-value-to-use-for-libopus-encoder-max-data-bytes-field)
/// for an explanation how this was calculated.
+/// Last validated on 2026-02-25 against RFC 6716.
const int maxDataBytes = 3 * 1275;
-/// Calculates, how much sampels a single opus package at [sampleRate] with [channels] may contain.
+/// Calculates, how many samples a single opus packet at [sampleRate] with [channels] may contain.
///
/// A single package may contain up 120ms of audio. This value is reached by combining up to 3 frames of 40ms audio.
int maxSamplesPerPacket(int sampleRate, int channels) =>
((sampleRate * channels * 120) / 1000).ceil();
/// Returns the version of the native libopus library.
-String getOpusVersion() {
- return _asString(opus.libinfo.opus_get_version_string());
-}
+String getOpusVersion() => _asString(opus.libinfo.opus_get_version_string());
+
+/// Upper bound for null-terminated string scans to prevent unbounded loops
+/// when a pointer is invalid or lacks a terminator.
+const int maxStringLength = 256;
String _asString(Pointer pointer) {
int i = 0;
- while (pointer[i] != 0) {
+ while (i < maxStringLength && pointer[i] != 0) {
i++;
}
+ if (i == maxStringLength) {
+ throw StateError(
+ '_asString: no null terminator found within $maxStringLength bytes');
+ }
return utf8.decode(pointer.asTypedList(i));
}
@@ -42,7 +55,7 @@ class OpusException implements Exception {
}
}
-/// Thrown when attempting to call an method on an already destroyed encoder or decoder.
+/// Thrown when attempting to call a method on an already destroyed encoder or decoder.
class OpusDestroyedError extends StateError {
OpusDestroyedError.encoder()
: super(
diff --git a/opus_dart/lib/src/opus_dart_packet.dart b/opus_dart/lib/src/opus_dart_packet.dart
index 5ed25b6..ca45d24 100644
--- a/opus_dart/lib/src/opus_dart_packet.dart
+++ b/opus_dart/lib/src/opus_dart_packet.dart
@@ -3,87 +3,58 @@ import 'dart:typed_data';
import '../wrappers/opus_defines.dart' as opus_defines;
import 'opus_dart_misc.dart';
-/// Bundles utility functions to examin opus packets.
+/// Bundles utility functions to examine opus packets.
///
/// All methods copy the input data into native memory.
abstract class OpusPacketUtils {
- /// Returns the amount of samples in a [packet] given a [sampleRate].
- static int getSampleCount(
- {required Uint8List packet, required int sampleRate}) {
+ static int _withNativePacket(
+ Uint8List packet, int Function(Pointer data) operation) {
Pointer data = opus.allocator.call(packet.length);
data.asTypedList(packet.length).setAll(0, packet);
try {
- int sampleCount = opus.decoder
- .opus_packet_get_nb_samples(data, packet.length, sampleRate);
- if (sampleCount < opus_defines.OPUS_OK) {
- throw OpusException(sampleCount);
+ int result = operation(data);
+ if (result < opus_defines.OPUS_OK) {
+ throw OpusException(result);
}
- return sampleCount;
+ return result;
} finally {
opus.allocator.free(data);
}
}
+ /// Returns the amount of samples in a [packet] given a [sampleRate].
+ static int getSampleCount(
+ {required Uint8List packet, required int sampleRate}) {
+ return _withNativePacket(
+ packet,
+ (data) => opus.decoder
+ .opus_packet_get_nb_samples(data, packet.length, sampleRate));
+ }
+
/// Returns the amount of frames in a [packet].
static int getFrameCount({required Uint8List packet}) {
- Pointer data = opus.allocator.call(packet.length);
- data.asTypedList(packet.length).setAll(0, packet);
- try {
- int frameCount =
- opus.decoder.opus_packet_get_nb_frames(data, packet.length);
- if (frameCount < opus_defines.OPUS_OK) {
- throw OpusException(frameCount);
- }
- return frameCount;
- } finally {
- opus.allocator.free(data);
- }
+ return _withNativePacket(packet,
+ (data) => opus.decoder.opus_packet_get_nb_frames(data, packet.length));
}
/// Returns the amount of samples per frame in a [packet] given a [sampleRate].
static int getSamplesPerFrame(
{required Uint8List packet, required int sampleRate}) {
- Pointer data = opus.allocator.call(packet.length);
- data.asTypedList(packet.length).setAll(0, packet);
- try {
- int samplesPerFrame =
- opus.decoder.opus_packet_get_samples_per_frame(data, sampleRate);
- if (samplesPerFrame < opus_defines.OPUS_OK) {
- throw OpusException(samplesPerFrame);
- }
- return samplesPerFrame;
- } finally {
- opus.allocator.free(data);
- }
+ return _withNativePacket(
+ packet,
+ (data) =>
+ opus.decoder.opus_packet_get_samples_per_frame(data, sampleRate));
}
/// Returns the channel count from a [packet]
static int getChannelCount({required Uint8List packet}) {
- Pointer data = opus.allocator.call(packet.length);
- data.asTypedList(packet.length).setAll(0, packet);
- try {
- int channelCount = opus.decoder.opus_packet_get_nb_channels(data);
- if (channelCount < opus_defines.OPUS_OK) {
- throw OpusException(channelCount);
- }
- return channelCount;
- } finally {
- opus.allocator.free(data);
- }
+ return _withNativePacket(
+ packet, (data) => opus.decoder.opus_packet_get_nb_channels(data));
}
/// Returns the bandwidth from a [packet]
static int getBandwidth({required Uint8List packet}) {
- Pointer data = opus.allocator.call(packet.length);
- data.asTypedList(packet.length).setAll(0, packet);
- try {
- int bandwidth = opus.decoder.opus_packet_get_bandwidth(data);
- if (bandwidth < opus_defines.OPUS_OK) {
- throw OpusException(bandwidth);
- }
- return bandwidth;
- } finally {
- opus.allocator.free(data);
- }
+ return _withNativePacket(
+ packet, (data) => opus.decoder.opus_packet_get_bandwidth(data));
}
}
diff --git a/opus_dart/lib/src/opus_dart_streaming.dart b/opus_dart/lib/src/opus_dart_streaming.dart
index 5d6b6fe..b909fea 100644
--- a/opus_dart/lib/src/opus_dart_streaming.dart
+++ b/opus_dart/lib/src/opus_dart_streaming.dart
@@ -55,7 +55,9 @@ class StreamOpusEncoder extends StreamTransformerBase, Uint8List> {
/// Indicates if the input data is interpreted as floats (`true`) or as s16le (`false`).
final bool floats;
- /// If `true`, the encoded output is copied into dart memory befor passig it to any consumers.
+ /// Previously controlled whether output was copied into Dart memory.
+ /// Output is now always copied for safety (prevents use-after-write hazards
+ /// when the native buffer is overwritten on the next encode call).
final bool copyOutput;
/// The sample rate in Hz for this encoder.
@@ -118,66 +120,112 @@ class StreamOpusEncoder extends StreamTransformerBase, Uint8List> {
maxInputBufferSizeBytes: (floats ? 4 : 2) *
_calculateMaxSampleSize(sampleRate, channels, frameTime));
+ /// Transforms an incoming PCM stream into encoded opus packets.
+ ///
+ /// The pipeline works in three stages:
+ /// 1. **Map** — converts typed input ([Int16List], [Float32List], or
+ /// [Uint8List]) into a uniform byte stream via [_mapStream].
+ /// 2. **Buffer & encode** — each byte chunk is fed into the encoder's
+ /// fixed-size input buffer via [_processChunk]. Every time the buffer
+ /// fills to exactly one frame, the frame is encoded and yielded.
+ /// 3. **Flush** — when the source stream closes, [_flushRemaining] handles
+ /// the partial frame: either zero-pads and encodes it
+ /// ([fillUpLastFrame] = true) or throws [UnfinishedFrameException].
+ ///
+ /// The encoder is always destroyed in the `finally` block, regardless of
+ /// whether the stream completes normally or with an error.
@override
Stream bind(Stream> stream) async* {
try {
- int dataIndex;
- Uint8List bytes;
- int available;
- int max;
- int use;
- Uint8List inputBuffer = _encoder.inputBuffer;
- Stream mapped;
- if (_expect == Int16List) {
- mapped = stream.cast().map((Int16List s16le) =>
- s16le.buffer.asUint8List(s16le.offsetInBytes, s16le.lengthInBytes));
- } else if (_expect == Float32List) {
- mapped = stream.cast().map((Float32List floats) => floats
- .buffer
- .asUint8List(floats.offsetInBytes, floats.lengthInBytes));
- } else {
- mapped = stream.cast();
- }
- await for (Uint8List pcm in mapped) {
- bytes = pcm;
- dataIndex = 0;
- available = bytes.lengthInBytes;
- while (available > 0) {
- max = _encoder.maxInputBufferSizeBytes - _encoder.inputBufferIndex;
- use = max < available ? max : available;
- inputBuffer.setRange(_encoder.inputBufferIndex,
- _encoder.inputBufferIndex + use, bytes, dataIndex);
- dataIndex += use;
- _encoder.inputBufferIndex += use;
- available = bytes.lengthInBytes - dataIndex;
- if (_encoder.inputBufferIndex == _encoder.maxInputBufferSizeBytes) {
- Uint8List bytes =
- floats ? _encoder.encodeFloat() : _encoder.encode();
- yield copyOutput ? Uint8List.fromList(bytes) : bytes;
- _encoder.inputBufferIndex = 0;
- }
+ await for (Uint8List pcm in _mapStream(stream)) {
+ for (final packet in _processChunk(pcm)) {
+ yield packet;
}
}
- if (_encoder.maxInputBufferSizeBytes != 0) {
- if (!fillUpLastFrame) {
- int missingSamples =
- (_encoder.maxInputBufferSizeBytes - _encoder.inputBufferIndex) ~/
- (floats ? 4 : 2);
- throw UnfinishedFrameException._(missingSamples: missingSamples);
- }
- _encoder.inputBuffer.setAll(
- _encoder.inputBufferIndex,
- Uint8List(
- _encoder.maxInputBufferSizeBytes - _encoder.inputBufferIndex));
- _encoder.inputBufferIndex = _encoder.maxInputBufferSizeBytes;
- Uint8List bytes = floats ? _encoder.encodeFloat() : _encoder.encode();
- yield copyOutput ? Uint8List.fromList(bytes) : bytes;
+ for (final packet in _flushRemaining()) {
+ yield packet;
}
} finally {
destroy();
}
}
+ /// Converts the typed input stream into a uniform [Stream].
+ ///
+ /// [Int16List] and [Float32List] elements are reinterpreted as their raw
+ /// byte representation without copying. [Uint8List] elements pass through
+ /// unchanged.
+ Stream _mapStream(Stream> stream) {
+ if (_expect == Int16List) {
+ return stream.cast