mirror of https://github.com/flutter/pinball.git
feat: better audio pool (#461)
* feat: better audio pool * cspell * lint * typo * coverage * Apply suggestions from code review Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> * suggestions * suggestions * pr * lint * lint * lint Co-authored-by: Allison Ryan <77211884+allisonryan0002@users.noreply.github.com> Co-authored-by: Tom Arra <tarra3@gmail.com>pull/463/head
parent
a73f464afe
commit
5492cfaef2
@ -0,0 +1,87 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:pinball_audio/pinball_audio.dart';
|
||||
|
||||
class _PlayerEntry {
|
||||
_PlayerEntry({
|
||||
required this.available,
|
||||
required this.player,
|
||||
});
|
||||
|
||||
bool available;
|
||||
final AudioPlayer player;
|
||||
}
|
||||
|
||||
/// {@template pinball_audio_pool}
|
||||
/// Creates an audio player pool used to trigger many sounds at the same time.
|
||||
/// {@endtemplate}
|
||||
class PinballAudioPool {
|
||||
/// {@macro pinball_audio_pool}
|
||||
PinballAudioPool({
|
||||
required this.path,
|
||||
required this.poolSize,
|
||||
required this.preCacheSingleAudio,
|
||||
required this.playSingleAudio,
|
||||
required this.duration,
|
||||
});
|
||||
|
||||
/// Sounds path.
|
||||
final String path;
|
||||
|
||||
/// Max size of this pool.
|
||||
final int poolSize;
|
||||
|
||||
/// Function to cache audios.
|
||||
final PreCacheSingleAudio preCacheSingleAudio;
|
||||
|
||||
/// Function to play audios.
|
||||
final PlaySingleAudio playSingleAudio;
|
||||
|
||||
/// How long the sound lasts.
|
||||
final Duration duration;
|
||||
|
||||
final List<_PlayerEntry> _players = [];
|
||||
|
||||
/// Loads the pool.
|
||||
Future<void> load() async {
|
||||
await preCacheSingleAudio(path);
|
||||
}
|
||||
|
||||
/// Plays the pool.
|
||||
Future<void> play({double volume = 1}) async {
|
||||
AudioPlayer? player;
|
||||
if (_players.length < poolSize) {
|
||||
_players.add(
|
||||
_PlayerEntry(
|
||||
available: false,
|
||||
player: player = await playSingleAudio(path, volume: volume),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final entries = _players.where((entry) => entry.available);
|
||||
if (entries.isNotEmpty) {
|
||||
final entry = entries.first..available = false;
|
||||
|
||||
player = entry.player;
|
||||
unawaited(entry.player.play(path, volume: volume));
|
||||
}
|
||||
}
|
||||
|
||||
if (player != null) {
|
||||
unawaited(
|
||||
Future<void>.delayed(duration).then(
|
||||
(_) {
|
||||
_returnEntryAvailability(player!);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {}
|
||||
}
|
||||
|
||||
void _returnEntryAvailability(
|
||||
AudioPlayer player,
|
||||
) {
|
||||
_players.where((entry) => entry.player == player).single.available = true;
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
// ignore_for_file: one_member_abstracts
|
||||
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:pinball_audio/src/pinball_audio_pool.dart';
|
||||
|
||||
class _MockAudioPlayer extends Mock implements AudioPlayer {}
|
||||
|
||||
class _MockPlaySingleAudio extends Mock {
|
||||
Future<AudioPlayer> onCall(String path, {double volume});
|
||||
}
|
||||
|
||||
abstract class _PreCacheSingleAudio {
|
||||
Future<void> onCall(String path);
|
||||
}
|
||||
|
||||
class _MockPreCacheSingleAudio extends Mock implements _PreCacheSingleAudio {}
|
||||
|
||||
void main() {
|
||||
group('PinballAudioPool', () {
|
||||
late _PreCacheSingleAudio preCacheSingleAudio;
|
||||
late _MockPlaySingleAudio playSingleAudio;
|
||||
late PinballAudioPool pool;
|
||||
late AudioPlayer audioPlayer;
|
||||
|
||||
setUp(() {
|
||||
preCacheSingleAudio = _MockPreCacheSingleAudio();
|
||||
when(() => preCacheSingleAudio.onCall(any())).thenAnswer((_) async {});
|
||||
|
||||
audioPlayer = _MockAudioPlayer();
|
||||
when(() => audioPlayer.play(any(), volume: any(named: 'volume')))
|
||||
.thenAnswer((_) async => 1);
|
||||
|
||||
playSingleAudio = _MockPlaySingleAudio();
|
||||
when(() => playSingleAudio.onCall(any(), volume: any(named: 'volume')))
|
||||
.thenAnswer((_) async => audioPlayer);
|
||||
|
||||
pool = PinballAudioPool(
|
||||
path: 'path',
|
||||
poolSize: 1,
|
||||
preCacheSingleAudio: preCacheSingleAudio.onCall,
|
||||
playSingleAudio: playSingleAudio.onCall,
|
||||
duration: const Duration(milliseconds: 10),
|
||||
);
|
||||
});
|
||||
|
||||
test('pre cache the sound', () async {
|
||||
await pool.load();
|
||||
verify(() => preCacheSingleAudio.onCall('path')).called(1);
|
||||
});
|
||||
|
||||
test('plays a fresh sound', () async {
|
||||
await pool.load();
|
||||
await pool.play();
|
||||
|
||||
verify(
|
||||
() => playSingleAudio.onCall(
|
||||
'path',
|
||||
volume: any(named: 'volume'),
|
||||
),
|
||||
).called(1);
|
||||
});
|
||||
|
||||
test('plays from the pool after it returned', () async {
|
||||
await pool.load();
|
||||
await pool.play();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 12));
|
||||
await pool.play();
|
||||
|
||||
verify(() => audioPlayer.play('path')).called(1);
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in new issue