xue_hua_audio 1.0.6 copy "xue_hua_audio: ^1.0.6" to clipboard
xue_hua_audio: ^1.0.6 copied to clipboard

Cross-platform Flutter FFI audio plugin — multi-track playback, progress streams, and microphone recording via Rust/rodio.

xue_hua_audio #

English | 简体中文

Cross-platform Flutter FFI audio plugin powered by Rust (rodio) and flutter_rust_bridge 2.12.

Features:

  • Multi-track playback — several sounds at once, each with independent volume, pause, and seek
  • Progress streams — push-based playback position (~100 ms), no Dart polling
  • Recording — microphone capture to WAV with level/duration streams
  • Multiple sources — local files, Flutter assets, and HTTP URLs

Supported platforms: Android, iOS, macOS, Linux, Windows.


Table of contents #


Installation #

Add to pubspec.yaml:

dependencies:
  xue_hua_audio: ^1.0.2

Call XueHuaAudio.initialize() once before any audio API (typically in main() after WidgetsFlutterBinding.ensureInitialized()).


Quick start #

import 'package:xue_hua_audio/xue_hua_audio.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final player = await XueHuaAudio.initialize();
  final engine = player.engine;

  // Play a bundled asset
  final track = await engine.loadAsset(assetKey: 'assets/audio/sample.wav');

  // Subscribe to progress (~100 ms)
  final sub = track.progressStream.listen((p) {
    print('${p.positionSecs.toStringAsFixed(1)} / ${p.durationSecs}');
  });

  // … later
  await sub.cancel();
  await track.stopAndCleanup();
  await player.dispose();
}

Architecture #

┌─────────────────────────────────────────────────────────┐
│  Dart (xue_hua_audio.dart)                        │
│  XueHuaAudio · extensions · TempFileRegistry      │
└──────────────────────────┬──────────────────────────────┘
                           │ flutter_rust_bridge
┌──────────────────────────▼──────────────────────────────┐
│  Rust                                                     │
│  XueHuaAudioEngine ──► MixerDeviceSink (system output)   │
│       ├── XueHuaAudioTrack × N  (Player + progress)     │
│       └── XueHuaAudioRecorder   (mic → WAV)             │
└───────────────────────────────────────────────────────────┘
  • Engine owns the system audio sink and track/recorder registries. The sink must stay alive for the app lifetime.
  • Track wraps one rodio Player connected to the shared mixer. Each track is independent.
  • TrackSharedState links engine stopAll() with track handles: deactivates the track and stops progress watchers.

Playback #

Loading audio #

Method Description
engine.loadLocal(path:, loop:) Absolute filesystem path; streamed decode on Rust side; loop: true repeats until stop()
engine.loadAsset(assetKey:, loop:) Reads Flutter asset → temp file → play
engine.loadUrl(url:, loop:) HTTP GET with timeout/retry → temp file → play

loop defaults to false. When enabled, playback uses rodio LoopedDecoder (streamed seek-back loop). Progress positionSecs / progress wrap each lap; call track.stop() to end.

XueHuaAudioTrack.replaceFromPath / replaceFromBytes also accept loop.

URL options are configured via XueHuaAudioOptions at initialize time:

await XueHuaAudio.initialize(
  options: XueHuaAudioOptions(
    urlTimeout: Duration(seconds: 30),
    urlMaxRetries: 2,
    urlRetryDelay: Duration(milliseconds: 500),
  ),
);

Asset and URL tracks register temp files in TempFileRegistry. Use track.stopAndCleanup() to stop and delete the temp file.

Track controls #

await track.pause();
await track.resume();
await track.setVolume(volume: 0.8);
await track.seekTo(positionSecs: 30.0);

// One-shot progress snapshot (sync)
final snap = track.playbackProgress();

// Stream (~100 ms)
track.progressStream.listen((p) { /* … */ });

await track.stop();           // stop + unregister
await track.stopAndCleanup(); // + delete temp file if Asset/URL

XueHuaPlaybackProgress #

Field Type Description
isPlaying bool Active and not paused/empty
isPaused bool Paused
isFinished bool Queue drained or track deactivated
positionSecs double Current position in seconds
durationSecs double? Total duration if known
progress double? Ratio 0.0–1.0 when duration is known

Initial UI can use track.playbackProgress() immediately; the stream updates thereafter.

Supported decode formats #

WAV, MP3, FLAC, Ogg Vorbis, MP4 (via rodio 0.22 + symphonia).


Recording #

final session = await engine.createRecordingSession();

session.progressStream.listen((p) {
  print('${p.durationSecs}s  level=${p.level}');
});

session.completedStream.listen((c) {
  print('Saved: ${c.outputPath}');
});

await session.start(
  outputPath: '/path/to/output.wav',
  deviceIndex: 0, // optional; default device if null
);

await session.pause();
await session.resume();

final path = await session.stop();
await session.dispose();

List input devices:

final devices = await engine.listInputDevices();

XueHuaRecordingProgress #

Field Description
isRecording Session active
isPaused Recording paused
durationSecs Elapsed recording time
level Recent buffer peak 0.0–1.0

Recording requires runtime microphone permission on mobile/desktop platforms (see Platform permissions reference).

Confirming recording completion #

Recording is not complete when you only pause — you must call stop() to finalize the WAV file.

Recommended (await stop()):

final path = await session.stop();
// `path` == outputPath passed to start(); WAV header/data is finalized
expect(session.isRecording, isFalse);

session.stop() blocks until the Rust writer thread joins, flushes samples, and finalizes the WAV on disk.

Optional: listen for the completed event

While recording, subscribe to completedStream. When the writer thread finishes, Rust pushes XueHuaRecordingCompleted:

late final StreamSubscription<XueHuaRecordingCompleted> completedSub;

completedSub = session.completedStream.listen((event) {
  print('Done: ${event.outputPath}, ${event.durationSecs}s');
  // event.outputPath — same path as start()
  // event.durationSecs — final recorded length
});

// … after user taps stop
final path = await session.stop();
await completedSub.cancel();

The Completed event is emitted before stop() returns, so you may receive it on completedStream slightly earlier than the Future completes. Using await session.stop() alone is sufficient for most apps.

Extra verification (optional):

import 'dart:io';

final file = File(path);
expect(await file.exists(), isTrue);
expect(await file.length(), greaterThan(44)); // non-empty WAV

After completion, session.isRecording is false. You can play the file with engine.loadLocal(path: path).


Lifecycle #

Always call track.stop() or track.stopAndCleanup() when done with a track. This:

  1. Stops the rodio player
  2. Unregisters from the engine
  3. Stops the progress watcher thread

Engine-wide stop #

engine.stopAll() or engine.stopAllWithCleanup() stops all players immediately:

  • Sets each track's shared state to inactive
  • Stops all progress watchers
  • playbackProgress().isFinished becomes true on held handles
  • track.stop() remains idempotent (safe to call again)
  • track.progressStream cannot be subscribed again (AlreadyStopped)

Do not rely on stopAll() alone if you need to clear Dart references and temp files — still call stopAndCleanup() per track.

Dispose #

await player.dispose();

Stops all tracks and recorders, cleans temp files, and releases the Rust engine. After dispose(), call initialize() again to reuse the plugin.


Errors #

XueHuaAudioError (Freezed sealed class):

Variant When
device Audio output/input device unavailable
localFile File open failure
decode Decode/seek/HTTP failure
alreadyStopped Operation on a stopped track
recording Recorder I/O or thread error
alreadyRecording start() while already recording
notRecording Pause/resume/stop when not recording

Platform setup #

Quick checklist — full details in Platform permissions reference.

Platform Playback Recording URL loading
Android No extra permission RECORD_AUDIO + runtime grant INTERNET in app manifest
iOS No extra permission NSMicrophoneUsageDescription + runtime grant App Transport Security (HTTPS default)
macOS No extra permission NSMicrophoneUsageDescription + Audio Input entitlement + runtime grant None
Linux None PulseAudio/PipeWire device access None
Windows None System microphone access None

The plugin merges RECORD_AUDIO from its own Android manifest. Your app must still request microphone permission at runtime before session.start().

Android NDK initialization #

On Android, rodio/cpal needs the JVM Context via ndk_context. When the Rust library is loaded only through Dart FFI (dlopen), that context is never set and XueHuaAudio.initialize() can panic with android context was not initialized.

Normal Flutter apps: add xue_hua_audio as a dependency — the plugin registers XueHuaAudioPlugin, which loads libxue_hua_audio.so through the JVM and initializes ndk_context before Dart main() runs. No MainActivity changes are required.

Custom Android embedders (non-standard Flutter embedding): load the native library from Java/Kotlin before any audio API, e.g. System.loadLibrary("xue_hua_audio"). See the flutter_rust_bridge Android NDK init guide.


Development #

Prerequisites #

  • Flutter SDK ≥ 3.3
  • Dart SDK ≥ 3.12
  • Rust toolchain (for local Rust builds)
  • flutter_rust_bridge_codegen 2.12

Dart analysis #

dart analyze lib example/lib

Integration tests (macOS) #

cd example
flutter test integration_test/playback_test.dart -d macos

Recording integration test is skipped by default (microphone not available in test harness); verify recording via the example app.


Example app #

cd example
flutter run

The demo includes:

  • Recording tab — device picker, record/pause/stop, level meter, playback of recorded WAV with progress
  • Playback tab — multi-sample playback, volume, seek, progress streams

See example/ for a full integration reference.


Platform permissions reference #

Per-platform permissions and declarations required in your host app (in addition to what the plugin already ships).

Android #

Item Required for Provided by plugin Required in your app
android.permission.RECORD_AUDIO Recording Yes (android/src/main/AndroidManifest.xml) Recommended duplicate in app manifest
Runtime mic permission Recording No Yes — e.g. permission_handler
android.permission.INTERNET loadUrl() No Yes, in app AndroidManifest.xml
minSdkVersion ≥ 26 All features Plugin default App must use ≥ 26
NDK / JVM context init Playback & recording Yes (XueHuaAudioPlugin) No — automatic when using standard Flutter embedding

App manifest (recording + URL):

<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.INTERNET"/>

Runtime request before recording:

import 'package:permission_handler/permission_handler.dart';

if (!await Permission.microphone.request().isGranted) {
  // handle denial
}
await session.start(outputPath: path);

iOS #

Item Required for In plugin In your app
NSMicrophoneUsageDescription Recording No Yesios/Runner/Info.plist
Runtime mic permission Recording No System prompt on first capture
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required to record audio.</string>

Playback-only apps do not need this key. Use HTTPS URLs or configure App Transport Security if loading HTTP audio.

macOS #

Item Required for In plugin In your app
NSMicrophoneUsageDescription Recording No Yesmacos/Runner/Info.plist
com.apple.security.device.audio-input Recording No Yes — entitlements file
Runtime mic permission Recording No System prompt on first capture

Info.plist:

<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is required to record audio.</string>

DebugProfile.entitlements / Release.entitlements:

<key>com.apple.security.device.audio-input</key>
<true/>

See example/macos/Runner/ for a working setup.

Linux #

Item Required for Notes
Audio output Playback Uses ALSA/PulseAudio via rodio; no manifest
Audio input Recording User must have mic device; may need desktop permission dialog (varies by DE)

No Flutter manifest entries. Ensure the process can access the default input device.

Windows #

Item Required for Notes
Audio output Playback WASAPI via rodio
Audio input Recording Microphone privacy settings — user must allow desktop apps to access the mic

No Info.plist or Android-style permissions. If recording fails, check Settings → Privacy → Microphone.

Summary #

Feature Android iOS macOS Linux Windows
Play local / asset
Play URL INTERNET ATS / HTTPS
Record RECORD_AUDIO + runtime NSMicrophoneUsageDescription + runtime plist + entitlement + runtime Device access Privacy settings

License #

See the repository license file. Version history: CHANGELOG.md.

0
likes
155
points
575
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

Cross-platform Flutter FFI audio plugin — multi-track playback, progress streams, and microphone recording via Rust/rodio.

Repository (GitHub)
View/report issues

License

Apache-2.0 (license)

Dependencies

flutter, flutter_rust_bridge, freezed_annotation, http, path, plugin_platform_interface

More

Packages that depend on xue_hua_audio

Packages that implement xue_hua_audio