-
Notifications
You must be signed in to change notification settings - Fork 853
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: AudioPool (moved and improved from flame_audio) (#1403)
This moves in the AudioPool class from flame_audio since it is completely free standing from Flame.
- Loading branch information
Showing
5 changed files
with
207 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); | ||
} |