flutter_pcm_sound
Envie áudio PCM em tempo real (inteiro de 16 bits) para os alto-falantes do seu dispositivo
Um plugin Flutter para reproduzir dados de áudio brutos PCM (inteiro de 16 bits)
{"sdk":"flutter"}Texto original em inglês. Visite o GitHub para a versão atual.
Logo
Send real-time PCM audio (16-bit integer) to your device speakers, from your Flutter app!
FlutterPcmSound has zero dependencies besides Flutter, Android, iOS, and MacOS themselves.
Unlike other plugins, flutter_pcm_sound does not use audio files (For example: flutter_soloud, sound_pool).
Instead, flutter_pcm_sound is for apps that generate audio in realtime a few milliseconds before you hear it. For example, using dart_melty_soundfont.
In contrast to raw_sound, FlutterPcmSound uses a callback setFeedCallback to signal when to feed more samples.
You can lower the feed threshold using setFeedThreshold to achieve real time audio, or increase it to have a cushy buffer.
Unlike traditional audio libraries which use a high-frequency timer-based audio callback, flutter_pcm_sound uses a low-frequency event-based callback. This integrates more seamlessly in the existing Flutter event loop, without necessitating an Isolate to ensure precise timing.
Your feed callback is invoked once for each of these events:
setFeedThreshold, i.e remainingFrames < threshold.remainingFrames == 0.Note: once means once per feed() — every time you feed new data, the plugin will trigger another low-buffer or zero event when necessary.
💡 Tip: By altering how many extra samples you
feedbeyond your threshold, you can control how oftenflutter_pcm_soundinvokes your feed callback.
💡 Tip: You should still consider running your sound code in a Dart
Isolate, so that it is decoupled from UI framedrops.
If you prefer, it's easy to wrap flutter_pcm_sound to simulate traditional timer-based feeding, i.e. invoking your feed callback at a specific preferrred rate, using a Dart timer.
Set a large feed threshold so that flutter_pcm_sound regularly tells you its remainingFrames
Start a Dart-side Timer.periodic(...) or Ticker
On each timer tick, call your feed callback with the estimated remaining frames. i.e. estimatedRemainingFrames = remainingFrames - (elapsedSeconds * sampleRate)
import 'dart:async';
import 'package:flutter_pcm_sound/flutter_pcm_sound.dart';
typedef FeedCallback = List<int> Function(int remainingFrames);
/// wraps FlutterPcmSound w/ timer-based feeding
class FlutterPcmTimer {
// --- config ---
static int _sampleRate = 48000;
static int _channelCount = 1;
static int _tickHz = 60;
// --- state ---
static FeedCallback? _onFeed;
static bool _isSetup = false;
static bool _playing = false;
static Timer? _timer;
// last native event snapshot
static int _lastEventFrames = 0;
static int _lastEventMicros = 0; // timestamp
// for UI
static bool get isPlaying => _playing;
static Future<void> setup({
int sampleRate = 48000,
int channelCount = 1,
int tickHz = 60,
IosAudioCategory iosAudioCategory = IosAudioCategory.playback,
bool iosAllowBackgroundAudio = false,
}) async {
_sampleRate = sampleRate;
_channelCount = channelCount;
_tickHz = tickHz;
await FlutterPcmSound.setup(
sampleRate: _sampleRate,
channelCount: _channelCount,
iosAudioCategory: iosAudioCategory,
iosAllowBackgroundAudio: iosAllowBackgroundAudio,
);
// Huge threshold → plugin regularly reports `remainingFrames`.
await FlutterPcmSound.setFeedThreshold(_sampleRate * 60 * 60 * 24 * 365);
FlutterPcmSound.setFeedCallback((remaining) {
_lastEventFrames = remaining;
_lastEventMicros = _nowMicros();
if (remaining == 0 && _playing) {
// Refill ASAP, but outside the native callback.
scheduleMicrotask(_tick);
}
});
_isSetup = true;
}
static void setFeedCallback(FeedCallback? cb) => _onFeed = cb;
static void start() {
if (!_isSetup) throw StateError('Call SoundTimer.setup(...) first.');
if (_playing) return;
_playing = true;
FlutterPcmSound.start();
final period = Duration(milliseconds: (1000 / _tickHz).round());
_timer ??= Timer.periodic(period, (_) => _tick());
}
static void stop() {
if (!_playing && _timer == null) return;
_playing = false;
_timer?.cancel();
_timer = null;
}
// --- internals ---
static void _tick() async {
if (!_playing) return;
final estRemaining = _estimatedRemainingFramesNow();
if (_onFeed == null) return;
final samples = _onFeed!(estRemaining);
if (samples.isEmpty) return;
if (!_playing) return; // guard before async
await FlutterPcmSound.feed(PcmArrayInt16.fromList(samples));
}
static int _estimatedRemainingFramesNow() {
final lastFrames = _lastEventFrames;
final lastMicros = _lastEventMicros;
if (lastMicros == 0) return 0;
final elapsedMicros = _nowMicros() - lastMicros;
final elapsedFrames = ((elapsedMicros / 1e6) * _sampleRate).round();
final est = lastFrames - elapsedFrames;
return est > 0 ? est : 0;
}
static int _nowMicros() => DateTime.now().microsecondsSinceEpoch;
}
To play audio, just keep calling feed.
To stop audio, just stop calling feed.
💡 Tip: If you prefer a traditional timer-based API with
start()andstop(), I recommend wrappingflutter_pcm_soundas described in Timer Based Feeding.
When your feed callback hits remainingFrames=0 you know playing stopped.
// for testing purposes, a C-Major scale
MajorScale scale = MajorScale(sampleRate: 44100, noteDuration: 0.25);
// invoked whenever we need to feed more samples to the platform
void onFeed(int remainingFrames) async {
// you could use 'remainingFrames' to feed very precisely.
// But here we just load a few thousand samples everytime we run low.
List<int> frame = scale.generate(periods: 20);
await FlutterPcmSound.feed(PcmArrayInt16.fromList(frame));
}
await FlutterPcmSound.setup(sampleRate: 44100, channelCount: 1);
await FlutterPcmSound.setFeedThreshold(8000);
FlutterPcmSound.setFeedCallback(onFeed);
FlutterPcmSound.start(); // for convenience. Equivalent to calling onFeed(0);
Please star this repo & on pub.dev. We all benefit from having a larger community.
Enable the platforms you need.
cd ./example
flutter config --enable-macos-desktop
flutter config --enable-android
flutter config --enable-ios
flutter create .
flutter run