xue_hua_audio

Live example: https://matkurban.github.io/xue_hua_audio/

English | 简体中文

Cross-platform native Flutter audio plugin — playback (file / URL / asset) and microphone recording with a real-time amplitude stream, on six platforms. No Rust toolchain, no FFI, no codegen: every platform is implemented with its first-class native audio API and wired up through a type-safe Pigeon channel.

Platform Playback Recording
Android Media3 ExoPlayer AudioRecord (WAV / AAC-LC)
iOS / macOS AVPlayer AVAudioEngine (WAV / AAC-LC)
Windows Media Foundation IMFMediaEngine WASAPI (WAV / AAC-LC)
Linux GStreamer playbin GStreamer level (WAV / Opus / AAC)
Web HTMLAudioElement MediaRecorder + AnalyserNode (Opus / MP4)

Features:

  • Playback — local files, network URLs (streamed, with custom headers), and Flutter assets; play / pause / stop / seek / volume / speed / loop.
  • Multi-instance — create as many AudioPlayers as you like; each owns its native resources and is disposed independently.
  • Recording — WAV / AAC-LC / Opus output, pause & resume, cancel, configurable sample rate / channels / bit rate, input device selection.
  • Device management — enumerate output/input devices, query the current one and route each player / recorder to a specific device.
  • Real-time amplitude — Stream<Amplitude> with current / max dBFS and a normalized 0–1 value, ready for waveform UIs.
  • Type-safe events — Streams for state, position, duration, and errors; structured AudioError (code + message + details) everywhere.
  • Bilingual docs — every public API carries English + Chinese dartdoc.

Migrating from 1.x (the Rust/FFI version)? See MIGRATION.md.


Installation

dependencies:
  xue_hua_audio: ^lasted

No initialization call is needed.

AI agent skills

This package ships Agent Skills that teach coding agents the 2.0 playback, recording, and platform-setup APIs. After adding the dependency, install them into your editor:

dart run skills@ get -p xue_hua_audio

Or install every skill from every dependency with dart run skills@ get --all. See Ship skills with packages.

Playback

import 'package:xue_hua_audio/xue_hua_audio.dart';

final player = AudioPlayer();

// Load any of the three source kinds. Returns the duration when known.
final duration = await player.setSource(
  AudioSource.url('https://example.com/song.mp3'),
);
// AudioSource.file('/path/to/local.mp3')
// AudioSource.asset('assets/audio/ring.wav')

await player.play();
await player.setVolume(0.8);   // 0.0 – 1.0
await player.setSpeed(1.5);    // 0.5 – 2.0
await player.setLooping(true);
await player.seek(const Duration(seconds: 10));
await player.pause();
await player.stop();

// Events
player.onStateChanged.listen((s) => print(s));    // PlayerState
player.onPositionChanged.listen((p) => print(p)); // every 100 ms (default)
player.onDurationChanged.listen((d) => print(d));
player.onError.listen((e) => print(e));

// Always release native resources when done.
await player.dispose();

PlayerState: idle → loading → ready → playing ⇄ paused → completed / stopped, plus error and terminal disposed.

Recording

final recorder = AudioRecorder();

if (await recorder.hasPermission()) {
  // Live waveform: normalized is 0–1, current/max are dBFS.
  recorder.onAmplitudeChanged.listen((a) => drawBar(a.normalized));

  await recorder.start(
    const RecordConfig(
      encoder: AudioEncoder.aacLc, // wav / aacLc / opus
      sampleRate: 44100,
      numChannels: 1,
    ),
    path: '/path/to/recording.m4a', // ignored on the Web
  );

  await recorder.pause();
  await recorder.resume();
  final path = await recorder.stop(); // file path, or blob URL on the Web
  // await recorder.cancel();         // stop and delete instead
}

await recorder.dispose();

Audio devices

Playback output device (per player):

final outputs = await AudioPlayer.listOutputDevices();
await player.setOutputDevice(outputs.first.id); // route this player
final current = await player.getOutputDevice(); // null = system default
await player.setOutputDevice(null);             // back to the default

Recording input device (per recorder):

final inputs = await recorder.listInputDevices();
await recorder.setInputDevice(inputs.first.id); // instance preference
final current = await recorder.getInputDevice(); // null = system default
// One-shot override for a single recording:
await recorder.start(RecordConfig(deviceId: inputs.first.id), path: ...);

Platform notes:

Platform Set output device Switch input while recording
Android ✅ immediate (API 23+) ✅
iOS ❌ unsupported (system routing) ✅ (setPreferredInput)
macOS ✅ immediate ❌ set before start
Windows ✅ from the next setSource (Win10 1703+) ❌ set before start
Linux ✅ from the next setSource ❌ set before start
Web ✅ immediate (setSinkId) ❌ set before start

On iOS listOutputDevices returns only the devices of the current audio route; on the Web labels may be empty until a media permission is granted.

Errors

Every async method throws a structured AudioError; the same errors are also published on player.onError / recorder.onError:

try {
  await player.setSource(AudioSource.file('/missing.mp3'));
} on AudioError catch (e) {
  print('${e.code}: ${e.message}'); // e.g. sourceNotFound: ...
}

Platform setup

Android

Nothing to do — the plugin manifest already declares RECORD_AUDIO and INTERNET. Minimum SDK 21.

iOS

Add to ios/Runner/Info.plist:

<key>NSMicrophoneUsageDescription</key>
<string>This app records audio from the microphone.</string>

Minimum iOS 13.0. The plugin manages AVAudioSession automatically.

macOS

Add NSMicrophoneUsageDescription to macos/Runner/Info.plist, and the following entitlements to both DebugProfile.entitlements and Release.entitlements:

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

Minimum macOS 10.15.

Windows

Nothing to do — Media Foundation and WASAPI ship with Windows 10+.

Linux

Build-time dependency on GStreamer development headers:

sudo apt install libgstreamer1.0-dev gstreamer1.0-plugins-good gstreamer1.0-plugins-bad

Web

  • Microphone access requires a secure context (HTTPS or localhost).
  • recorder.stop() returns a blob URL; the actual container/codec depends on the browser (audio/webm;codecs=opus on Chrome/Firefox, audio/mp4 on Safari) regardless of the requested encoder.
  • Remote audio playback is subject to CORS.

Architecture

This is a federated plugin:

xue_hua_audio                     ← the package apps depend on
└── xue_hua_audio_platform_interface
    ├── xue_hua_audio_android     Kotlin  · ExoPlayer + AudioRecord
    ├── xue_hua_audio_darwin      Swift   · AVPlayer + AVAudioEngine (iOS+macOS)
    ├── xue_hua_audio_web         Dart    · package:web
    ├── xue_hua_audio_windows     C++     · Media Foundation + WASAPI
    └── xue_hua_audio_linux       C       · GStreamer

Commands travel over Pigeon-generated type-safe channels; events (state, position, duration, amplitude, errors) come back per-instance over an EventChannel. The Web implementation is pure Dart and skips channels entirely.

Example app

The example demonstrates all three source kinds, seek / volume / speed / loop controls, and recording with a live waveform.

Live demo: matkurban.github.io/xue_hua_audio/

License

MIT.

Libraries

xue_hua_audio
Cross-platform native audio playback and recording for Flutter.