429 lines
14 KiB
Dart
429 lines
14 KiB
Dart
import 'dart:async';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:soundpool_platform_interface/soundpool_platform_interface.dart';
|
|
|
|
import 'src/platform_options.dart';
|
|
|
|
export 'src/platform_options.dart';
|
|
|
|
class Soundpool {
|
|
static const _DEFAULT_SOUND_PRIORITY = 1;
|
|
|
|
final int _maxStreams;
|
|
final StreamType _streamType;
|
|
final Map<String, dynamic> _platformOptions;
|
|
final Completer<int> _soundpoolId = Completer();
|
|
|
|
SoundpoolPlatform get _platformInstance => SoundpoolPlatform.instance;
|
|
|
|
bool _disposed = false;
|
|
|
|
Soundpool._(this._platformOptions, [StreamType type = StreamType.music, int maxStreams = 1])
|
|
: assert(maxStreams > 0),
|
|
_streamType = type,
|
|
_maxStreams = maxStreams;
|
|
|
|
@override
|
|
String toString() {
|
|
return 'Soundpool($_streamType:$hashCode)';
|
|
}
|
|
|
|
/// Creates the Soundpool instance with stream type setting set.
|
|
/// Soundpool can play up to [maxStreams] of simultaneous streams
|
|
///
|
|
/// *Note:* Optional [streamType] parameter has effect on Android only.
|
|
@Deprecated('Use `fromOptions` instead')
|
|
factory Soundpool({StreamType streamType = StreamType.music, int maxStreams = 1}) =>
|
|
Soundpool.fromOptions(
|
|
options: SoundpoolOptions(
|
|
streamType: streamType,
|
|
maxStreams: maxStreams,
|
|
),
|
|
);
|
|
|
|
/// Creates the Soundpool instance with stream type setting set.
|
|
factory Soundpool.fromOptions({SoundpoolOptions options = SoundpoolOptions.kDefault}) {
|
|
return Soundpool._(
|
|
options._platformOptions,
|
|
options.streamType,
|
|
options.maxStreams,
|
|
).._connect();
|
|
}
|
|
|
|
/// Prepares sound for playing
|
|
///
|
|
/// ```
|
|
/// load(await rootBundle.load("sounds/dices.m4a")); // loads file from assets
|
|
/// ```
|
|
///
|
|
/// Returns soundId for future use in [play] (soundId > -1) or `-1` when sound file failed to load
|
|
///
|
|
/// ## web
|
|
/// [priority] is ignored.
|
|
Future<int> load(ByteData rawSound, {int priority = _DEFAULT_SOUND_PRIORITY}) =>
|
|
loadUint8List(rawSound.buffer.asUint8List(), priority: priority);
|
|
|
|
/// Prepares sound for playing
|
|
///
|
|
/// Loads sound data and buffers it for future playing.
|
|
///
|
|
/// Returns soundId for future use in [play] (soundId > -1) or `-1` when sound data failed to load
|
|
///
|
|
/// ## web
|
|
/// [priority] is ignored.
|
|
Future<int> loadUint8List(Uint8List rawSound, {int priority = _DEFAULT_SOUND_PRIORITY}) async {
|
|
assert(!_disposed, "Soundpool instance was already disposed");
|
|
int poolId = await _soundpoolId.future;
|
|
int soundId = await _platformInstance.loadUint8List(poolId, rawSound, priority);
|
|
return soundId;
|
|
}
|
|
|
|
/// Prepares sound for playing
|
|
///
|
|
/// Loads sound data from file pointed by [uri]
|
|
/// Returns soundId for future use in [play] (soundId > -1) or `-1` when sound file failed to load
|
|
///
|
|
/// ## web
|
|
/// [priority] is ignored.
|
|
Future<int> loadUri(String uri, {int priority = _DEFAULT_SOUND_PRIORITY}) async {
|
|
assert(!_disposed, "Soundpool instance was already disposed");
|
|
int poolId = await _soundpoolId.future;
|
|
int soundId = await _platformInstance.loadUri(poolId, uri, priority);
|
|
return soundId;
|
|
}
|
|
|
|
/// Prepares sound for playing and plays immediately when loaded
|
|
///
|
|
/// ```
|
|
/// loadAndPlay(await rootBundle.load("sounds/dices.m4a")); // loads file from assets
|
|
/// ```
|
|
///
|
|
/// Returns soundId for future use in [play] (soundId > -1) or `-1` when sound file failed to load
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [load], which allows for precaching the sound data
|
|
///
|
|
/// ## web
|
|
/// [priority] and [repeat] are ignored. The sound is played only once.
|
|
Future<int> loadAndPlay(ByteData rawSound,
|
|
{int priority = _DEFAULT_SOUND_PRIORITY, int repeat = 0, double rate = 1.0}) async {
|
|
int soundId = await load(rawSound, priority: priority);
|
|
if (soundId > -1) {
|
|
play(soundId, repeat: repeat, rate: rate);
|
|
}
|
|
return soundId;
|
|
}
|
|
|
|
/// Prepares sound for playing and plays immediately after loading
|
|
///
|
|
/// Loads sound data, buffers it for future playing and starts playing immediately
|
|
/// when loaded.
|
|
///
|
|
/// Returns soundId for future use in [play] (soundId > -1) or `-1` when sound file failed to load
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [loadUint8List], which allows for precaching the sound data
|
|
///
|
|
/// ## web
|
|
/// [priority] and [repeat] are ignored. The sound is played only once.
|
|
Future<int> loadAndPlayUint8List(Uint8List rawSound,
|
|
{int priority = _DEFAULT_SOUND_PRIORITY, int repeat = 0, double rate = 1.0}) async {
|
|
assert(!_disposed, "Soundpool instance was already disposed");
|
|
int soundId = await loadUint8List(rawSound, priority: priority);
|
|
if (soundId > -1) {
|
|
play(soundId, repeat: repeat, rate: rate);
|
|
}
|
|
return soundId;
|
|
}
|
|
|
|
/// Prepares sound for playing and plays immediately after loading
|
|
///
|
|
/// Loads sound data from file pointed by [uri]
|
|
///
|
|
/// Returns soundId for future use in [play] (soundId > -1) or `-1` when sound file failed to load
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [loadUri], which allows for precaching the sound data
|
|
///
|
|
/// ## web
|
|
/// [priority] and [repeat] are ignored. The sound is played only once.
|
|
Future<int> loadAndPlayUri(String uri,
|
|
{int priority = _DEFAULT_SOUND_PRIORITY, int repeat = 0, double rate = 1.0}) async {
|
|
assert(!_disposed, "Soundpool instance was already disposed");
|
|
int soundId = await loadUri(uri, priority: priority);
|
|
if (soundId > -1) {
|
|
play(soundId, repeat: repeat, rate: rate);
|
|
}
|
|
return soundId;
|
|
}
|
|
|
|
/// Plays sound identified by [soundId]
|
|
///
|
|
/// Returns streamId to further control playback or 0 if playing failed to
|
|
/// start
|
|
///
|
|
/// ## web
|
|
/// [repeat] is ignored. The sound is played only once.
|
|
Future<int> play(int soundId, {int repeat = 0, double rate = 1.0}) async {
|
|
assert(!_disposed, "Soundpool instance was already disposed");
|
|
assert(
|
|
rate >= 0.5 && rate <= 2.0,
|
|
"'rate' has to be value in (0.5 - 2.0) range",
|
|
);
|
|
assert(soundId > -1, "Invalid 'soundId' parameter. Only values greater than -1 are valid.");
|
|
int poolId = await _soundpoolId.future;
|
|
return await _platformInstance.play(poolId, soundId, repeat, rate);
|
|
}
|
|
|
|
/// Starts playing the sound identified by [soundId].
|
|
///
|
|
/// Returns instance to control playback
|
|
///
|
|
/// ## web
|
|
/// [repeat] is ignored. The sound is played only once.
|
|
Future<AudioStreamControl> playWithControls(int soundId,
|
|
{int repeat = 0, double rate = 1.0}) async {
|
|
final streamId = await play(soundId, repeat: repeat, rate: rate);
|
|
return AudioStreamControl._(this, streamId);
|
|
}
|
|
|
|
/// Stops playing sound identified by [streamId]
|
|
///
|
|
///
|
|
Future<void> stop(int streamId) async {
|
|
assert(!_disposed, "Soundpool instance was already disposed");
|
|
assert(streamId > 0, "Invalid 'streamId' parameter. Only values greater than 0 are valid.");
|
|
int poolId = await _soundpoolId.future;
|
|
await _platformInstance.stop(poolId, streamId);
|
|
}
|
|
|
|
/// Pauses playing sound identified by [streamId]
|
|
///
|
|
/// ## web
|
|
/// *DOES NOT WORK!*.
|
|
Future<void> pause(int streamId) async {
|
|
assert(!_disposed, "Soundpool instance was already disposed");
|
|
assert(streamId > 0, "Invalid 'streamId' parameter. Only values greater than 0 are valid.");
|
|
int poolId = await _soundpoolId.future;
|
|
await _platformInstance.pause(poolId, streamId);
|
|
}
|
|
|
|
/// Resumes playing sound identified by [streamId]
|
|
///
|
|
/// ## web
|
|
/// *DOES NOT WORK!*.
|
|
Future<void> resume(int streamId) async {
|
|
assert(!_disposed, "Soundpool instance was already disposed");
|
|
assert(streamId > 0, "Invalid 'streamId' parameter. Only values greater than 0 are valid.");
|
|
int poolId = await _soundpoolId.future;
|
|
await _platformInstance.resume(poolId, streamId);
|
|
}
|
|
|
|
/// Sets volume for playing sound identified by [soundId] or [streamId]
|
|
///
|
|
/// At least [volume] or both [volumeLeft] and [volumeRight] have to be passed
|
|
///
|
|
/// ## web
|
|
/// [volumeLeft] and [volumeRight] pair has no effect.
|
|
Future<void> setVolume(
|
|
{int? soundId,
|
|
int? streamId,
|
|
double? volume,
|
|
double? volumeLeft,
|
|
double? volumeRight}) async {
|
|
assert(!_disposed, "Soundpool instance was already disposed");
|
|
assert(
|
|
soundId != null || streamId != null,
|
|
"Either 'soundId' or 'streamI"
|
|
"d' has to be passed");
|
|
assert(
|
|
volume != null || (volumeLeft != null && volumeRight != null),
|
|
"Ei"
|
|
"ther 'volume' or both 'volumeLeft' and 'volumeRight' has to be "
|
|
"passed");
|
|
|
|
assert(streamId == null || streamId > 0,
|
|
"Invalid 'streamId' parameter. Only values greater than 0 are valid.");
|
|
assert(soundId == null || soundId > -1,
|
|
"Invalid 'soundId' parameter. Only values greater than -1 are valid.");
|
|
|
|
if (volume != null && volumeLeft == null) {
|
|
volumeLeft = volume;
|
|
}
|
|
if (volume != null && volumeRight == null) {
|
|
volumeRight = volume;
|
|
}
|
|
await _soundpoolId.future.then((poolId) =>
|
|
_platformInstance.setVolume(poolId, soundId, streamId, volumeLeft, volumeRight));
|
|
}
|
|
|
|
/// Sets playback rate. A value of 1.0 means normal speed, 0.5 - half speed, 2.0 - double speed.
|
|
///
|
|
/// Available value range: (0.5 - 2.0)
|
|
Future<void> setRate({required int streamId, required double playbackRate}) async {
|
|
assert(!_disposed, "Soundpool instance was already disposed");
|
|
assert(streamId > 0, "Invalid 'streamId' parameter. Only values greater than 0 are valid.");
|
|
assert(
|
|
playbackRate >= 0.5 && playbackRate <= 2.0,
|
|
"'playbackRate' has to be value in (0.5 - 2.0) range",
|
|
);
|
|
await _soundpoolId.future
|
|
.then((poolId) => _platformInstance.setRate(poolId, streamId, playbackRate));
|
|
}
|
|
|
|
/// Releases loaded sounds
|
|
///
|
|
/// Should be called to clear buffered sounds
|
|
Future<void> release() async {
|
|
assert(!_disposed, "Soundpool instance was already disposed");
|
|
await _soundpoolId.future.then((poolId) => _platformInstance.release(poolId));
|
|
}
|
|
|
|
/// Disposes soundpool
|
|
///
|
|
/// The Soundpool instance is not usable anymore
|
|
void dispose() {
|
|
_soundpoolId.future.then((poolId) => _platformInstance.dispose(poolId), onError: (_) {});
|
|
_disposed = true;
|
|
}
|
|
|
|
StreamType get streamType => _streamType;
|
|
|
|
/// Connects to native Soundpool instance
|
|
_connect() async {
|
|
final int id = await _platformInstance.init(_streamType.index, _maxStreams, _platformOptions);
|
|
if (id >= 0) {
|
|
_soundpoolId.complete(id);
|
|
} else {
|
|
_soundpoolId.completeError("Soundpool initialization failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The type of the audio stream. Different streams may have distinct audio
|
|
/// settings (e.g. volume level) within the system
|
|
///
|
|
/// All sounds for particular Soundpool would be played using the selected stream
|
|
enum StreamType {
|
|
/// Audio stream for the phone ring
|
|
ring,
|
|
|
|
/// Audio stream for alarms
|
|
alarm,
|
|
|
|
/// Audio stream for music playbacks
|
|
music,
|
|
|
|
/// Audio stream for notifications
|
|
notification
|
|
}
|
|
|
|
/// Controls for played sound
|
|
///
|
|
/// Utility class that wraps the stream id with a easy-to-use API
|
|
class AudioStreamControl {
|
|
final Soundpool _pool;
|
|
bool _playing = true;
|
|
bool _stopped = false;
|
|
|
|
/// Id of the stream that is controlled by this object
|
|
final int stream;
|
|
|
|
/// Returns true if stream is not paused
|
|
///
|
|
/// This does not reflect actual player state. The sound may have been finished
|
|
/// any moment before by reaching the end
|
|
bool get playing => _playing;
|
|
|
|
/// Returns true if stream has been stopped and cannot be resumed
|
|
///
|
|
/// This does not reflect actual player state. The sound may have been finished
|
|
/// any moment before by reaching the end
|
|
bool get stopped => _stopped;
|
|
|
|
AudioStreamControl._(this._pool, this.stream);
|
|
|
|
/// Stops playing the stream associated with this object
|
|
Future<void> stop() async {
|
|
await _pool.stop(stream);
|
|
_stopped = true;
|
|
_playing = false;
|
|
}
|
|
|
|
/// Pauses playing the stream associated with this object
|
|
Future<void> pause() async {
|
|
if (!_stopped && _playing) {
|
|
await _pool.pause(stream);
|
|
_playing = false;
|
|
}
|
|
}
|
|
|
|
/// Resumes paused stream associated with this object
|
|
Future<void> resume() async {
|
|
if (!_stopped && _playing) {
|
|
await _pool.pause(stream);
|
|
_playing = true;
|
|
}
|
|
}
|
|
|
|
/// Sets volume for playing sound identified by [soundId] or [streamId]
|
|
///
|
|
/// At least [volume] or both [volumeLeft] and [volumeRight] have to be passed
|
|
Future setVolume({double? volume, double? volumeLeft, double? volumeRight}) {
|
|
return _pool.setVolume(
|
|
streamId: stream, volume: volume, volumeLeft: volumeLeft, volumeRight: volumeRight);
|
|
}
|
|
|
|
/// Sets playback rate. A value of 1.0 means normal speed, 0.5 - half speed, 2.0 - double speed.
|
|
///
|
|
/// Available value range: (0.5 - 2.0)
|
|
Future setRate({required double playbackRate}) {
|
|
return _pool.setRate(
|
|
streamId: stream,
|
|
playbackRate: playbackRate,
|
|
);
|
|
}
|
|
}
|
|
|
|
class SoundpoolOptions {
|
|
/// The type of stream used by the pool
|
|
final StreamType streamType;
|
|
|
|
/// Maximum number of pararell sounds being played
|
|
final int maxStreams;
|
|
|
|
/// Android specific options
|
|
final SoundpoolOptionsAndroid androidOptions;
|
|
|
|
/// iOS specific options
|
|
final SoundpoolOptionsIos iosOptions;
|
|
|
|
/// Web specific options
|
|
final SoundpoolOptionsWeb webOptions;
|
|
|
|
/// MacOS specific options
|
|
final SoundpoolOptionsMacos macosOptions;
|
|
|
|
const SoundpoolOptions({
|
|
this.streamType = StreamType.music,
|
|
this.maxStreams = 1,
|
|
this.androidOptions = SoundpoolOptionsAndroid.kDefault,
|
|
this.iosOptions = SoundpoolOptionsIos.kDefault,
|
|
this.webOptions = SoundpoolOptionsWeb.kDefault,
|
|
this.macosOptions = SoundpoolOptionsMacos.kDefault,
|
|
});
|
|
|
|
static const kDefault = SoundpoolOptions();
|
|
|
|
Map<String, dynamic> get _platformOptions => Map.fromEntries([
|
|
...androidOptions.toOptionsMap().entries.map((e) => MapEntry('android_${e.key}', e.value)),
|
|
...iosOptions.toOptionsMap().entries.map((e) => MapEntry('ios_${e.key}', e.value)),
|
|
...webOptions.toOptionsMap().entries.map((e) => MapEntry('web_${e.key}', e.value)),
|
|
...macosOptions.toOptionsMap().entries.map((e) => MapEntry('macos_${e.key}', e.value)),
|
|
]);
|
|
}
|