Skip to content

Commit

Permalink
feat: AudioPool (moved and improved from flame_audio) (#1403)
Browse files Browse the repository at this point in the history
This moves in the AudioPool class from flame_audio since it is completely free standing from Flame.
  • Loading branch information
spydon authored Jan 29, 2023
1 parent 4ea5907 commit ab15cb0
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/audioplayers/lib/audioplayers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ export 'package:audioplayers_platform_interface/api/release_mode.dart';
export 'package:audioplayers_platform_interface/global_platform_interface.dart';

export 'src/audio_cache.dart';
export 'src/audio_pool.dart';
export 'src/audioplayer.dart';
export 'src/source.dart';
126 changes: 126 additions & 0 deletions packages/audioplayers/lib/src/audio_pool.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import 'dart:async';

import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/foundation.dart';
import 'package:synchronized/synchronized.dart';

/// Represents a function that can stop an audio playing.
typedef StopFunction = Future<void> Function();

/// An AudioPool is a provider of AudioPlayers that are pre-loaded with an asset
/// to minimize delays.
///
/// All AudioPlayers are loaded with the same audio [source].
/// If you want multiple sounds use multiple [AudioPool]s.
///
/// Use this class if you for example have extremely quick firing, repetitive
/// or simultaneous sounds.
class AudioPool {
@visibleForTesting
final Map<String, AudioPlayer> currentPlayers = {};
@visibleForTesting
final List<AudioPlayer> availablePlayers = [];

/// Instance of [AudioCache] to be used by all players.
final AudioCache audioCache;

/// The source of the sound for this pool.
final Source source;

/// The minimum numbers of players, this is the amount of players that the
/// pool is initialized with.
final int minPlayers;

/// The maximum number of players to be kept in the pool.
///
/// If `start` is called after the pool is full there will still be new
/// [AudioPlayer]s created, but once they are stopped they will not be
/// returned to the pool.
final int maxPlayers;

final Lock _lock = Lock();

AudioPool._({
required this.minPlayers,
required this.maxPlayers,
required this.source,
AudioCache? audioCache,
}) : audioCache = audioCache ?? AudioCache.instance;

/// Creates an [AudioPool] instance with the given parameters.
static Future<AudioPool> create({
required Source source,
AudioCache? audioCache,
int minPlayers = 1,
required int maxPlayers,
}) async {
final instance = AudioPool._(
source: source,
audioCache: audioCache,
maxPlayers: maxPlayers,
minPlayers: minPlayers,
);

final players = await Future.wait(
List.generate(minPlayers, (_) => instance._createNewAudioPlayer()),
);

return instance..availablePlayers.addAll(players);
}

/// Creates an [AudioPool] instance with the asset from the given [path].
static Future<AudioPool> createFromAsset({
required String path,
AudioCache? audioCache,
int minPlayers = 1,
required int maxPlayers,
}) async {
return create(
source: AssetSource(path),
audioCache: audioCache,
minPlayers: minPlayers,
maxPlayers: maxPlayers,
);
}

/// Starts playing the audio, returns a function that can stop the audio.
Future<StopFunction> start({double volume = 1.0}) async {
return _lock.synchronized(() async {
if (availablePlayers.isEmpty) {
availablePlayers.add(await _createNewAudioPlayer());
}
final player = availablePlayers.removeAt(0);
currentPlayers[player.playerId] = player;
await player.setVolume(volume);
await player.resume();

late StreamSubscription<void> subscription;

Future<void> stop() {
return _lock.synchronized(() async {
final removedPlayer = currentPlayers.remove(player.playerId);
if (removedPlayer != null) {
subscription.cancel();
await removedPlayer.stop();
if (availablePlayers.length >= maxPlayers) {
await removedPlayer.release();
} else {
availablePlayers.add(removedPlayer);
}
}
});
}

subscription = player.onPlayerComplete.listen((_) => stop());

return stop;
});
}

Future<AudioPlayer> _createNewAudioPlayer() async {
final player = AudioPlayer()..audioCache = audioCache;
await player.setSource(source);
await player.setReleaseMode(ReleaseMode.stop);
return player;
}
}
1 change: 1 addition & 0 deletions packages/audioplayers/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dependencies:
sdk: flutter
http: ^0.13.5
path_provider: ^2.0.12
synchronized: ^3.0.1
uuid: ^3.0.7

dev_dependencies:
Expand Down
Binary file added packages/audioplayers/test/assets/empty.mp3
Binary file not shown.
79 changes: 79 additions & 0 deletions packages/audioplayers/test/audio_pool_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';

import 'audio_cache_test.dart';

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

const _channel = MethodChannel('plugins.flutter.io/path_provider');
_channel.setMockMethodCallHandler((c) async => '/tmp');

const channel = MethodChannel('xyz.luan/audioplayers');
channel.setMockMethodCallHandler((MethodCall call) async => 1);

group('AudioPool', () {
test('creates instance', () async {
final pool = await AudioPool.createFromAsset(
path: 'audio.mp3',
maxPlayers: 3,
audioCache: MyAudioCache(),
);
final stop = await pool.start();

expect((pool.source as AssetSource).path, 'audio.mp3');
expect(pool.audioCache.loadedFiles.keys.first, 'audio.mp3');
stop();
expect((pool.source as AssetSource).path, 'audio.mp3');
});

test('multiple players running', () async {
final pool = await AudioPool.createFromAsset(
path: 'audio.mp3',
maxPlayers: 3,
audioCache: MyAudioCache(),
);
final stop1 = await pool.start();
final stop2 = await pool.start();
final stop3 = await pool.start();

expect((pool.source as AssetSource).path, 'audio.mp3');
expect(pool.audioCache.loadedFiles.keys.first, 'audio.mp3');
expect(pool.availablePlayers.isEmpty, isTrue);
expect(pool.currentPlayers.length, 3);

await stop1();
await stop2();
await stop3();
expect(pool.availablePlayers.length, 3);
expect(pool.currentPlayers.isEmpty, isTrue);
});

test('keeps the minPlayers/maxPlayers contract', () async {
final pool = await AudioPool.createFromAsset(
path: 'audio.mp3',
maxPlayers: 3,
audioCache: MyAudioCache(),
);
final stopFunctions =
await Future.wait(List.generate(5, (_) => pool.start()));

expect(pool.availablePlayers.isEmpty, isTrue);
expect(pool.currentPlayers.length, 5);

await stopFunctions[0]();
await stopFunctions[1]();

expect(pool.availablePlayers.length, 2);
expect(pool.currentPlayers.length, 3);

await stopFunctions[2]();
await stopFunctions[3]();
await stopFunctions[4]();

expect(pool.availablePlayers.length, 3);
expect(pool.currentPlayers.isEmpty, isTrue);
});
});
}

0 comments on commit ab15cb0

Please sign in to comment.